├── .github
└── img
│ └── share_screenshot.jpg
├── .gitignore
├── LICENSE
├── README.md
├── Shaarlier.iml
├── TODO.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── dimtion
│ │ └── shaarlier
│ │ └── network
│ │ ├── NetworkUtilsTest.java
│ │ └── RestAPINetworkManagerTest.java
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── dimtion
│ │ └── shaarlier
│ │ ├── activities
│ │ ├── AccountsManagementActivity.java
│ │ ├── AddAccountActivity.java
│ │ ├── HttpSchemeHandlerActivity.java
│ │ ├── MainActivity.java
│ │ └── ShareActivity.java
│ │ ├── dao
│ │ ├── AccountsSource.java
│ │ ├── MySQLiteHelper.java
│ │ └── TagsSource.java
│ │ ├── exceptions
│ │ └── UnsupportedIntent.java
│ │ ├── helper
│ │ ├── AutoCompleteWrapper.java
│ │ ├── DebugHelper.java
│ │ ├── EncryptionHelper.java
│ │ └── ShareIntentParser.java
│ │ ├── models
│ │ ├── Link.java
│ │ ├── ShaarliAccount.java
│ │ └── Tag.java
│ │ ├── network
│ │ ├── MockNetworkManager.java
│ │ ├── NetworkManager.java
│ │ ├── NetworkUtils.java
│ │ ├── PasswordNetworkManager.java
│ │ └── RestAPINetworkManager.java
│ │ ├── services
│ │ └── NetworkService.java
│ │ └── utils
│ │ ├── FuzzyArrayAdapter.java
│ │ └── UserPreferences.java
│ └── res
│ ├── drawable-anydpi
│ └── ic_send_white.xml
│ ├── drawable-hdpi
│ ├── ic_action_accept.png
│ ├── ic_action_new.png
│ ├── ic_launcher.png
│ └── ic_send_white.png
│ ├── drawable-mdpi
│ ├── ic_action_accept.png
│ ├── ic_action_new.png
│ ├── ic_launcher.png
│ └── ic_send_white.png
│ ├── drawable-xhdpi
│ ├── ic_action_accept.png
│ ├── ic_action_new.png
│ ├── ic_launcher.png
│ └── ic_send_white.png
│ ├── drawable-xxhdpi
│ ├── ic_action_accept.png
│ ├── ic_action_new.png
│ ├── ic_launcher.png
│ └── ic_send_white.png
│ ├── drawable-xxxhdpi
│ └── ic_launcher.png
│ ├── drawable
│ └── shaarli_icon.png
│ ├── layout
│ ├── activity_accounts_management.xml
│ ├── activity_add_account.xml
│ ├── activity_main.xml
│ ├── activity_share.xml
│ └── tags_list.xml
│ ├── menu
│ ├── menu_accounts_management.xml
│ ├── menu_add_account.xml
│ ├── menu_main.xml
│ └── menu_share.xml
│ ├── values-fr
│ └── strings.xml
│ ├── values-sk
│ └── strings.xml
│ ├── values-w820dp
│ └── dimens.xml
│ └── values
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.github/img/share_screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/.github/img/share_screenshot.jpg
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | *.apk
9 | *.aab
10 | *.log
11 | *Thumbs.db
12 | app/src/main/gen/*
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Shaarlier
2 |
3 | 
4 | [](https://www.codacy.com/app/zizou-xena/Shaarlier)
5 | 
6 |
7 | A simple Android app for publishing links on your Shaarli instance from android share menu
8 |
9 | 
10 |
11 | ## Installation
12 | - From the [Play Store](https://play.google.com/store/apps/details?id=com.dimtion.shaarlier)
13 | - From [F-Droid](https://f-droid.org/en/packages/com.dimtion.shaarlier/)
14 | - From the [release tab](https://github.com/dimtion/Shaarlier/releases)
15 |
16 | ## Features
17 | - Publish links on your Shaarli from the Android Share menu
18 | - Automatically add a title and a description
19 | - Optional dialog box for editing title, description or tags
20 | - Autocomplete tags
21 | - Edit existing shares
22 | - Multiple Shaarli accounts
23 | - Works either via Shaarli REST API or via username/password combo
24 |
25 | ## Planned features
26 | See [the next milestones](https://github.com/dimtion/Shaarlier/milestones)
27 |
28 | ## Links
29 | - Main and recommended fork of the Shaarli project: https://github.com/shaarli/Shaarli/
30 | - Managed Shaarli instances using: https://my.framasoft.org/
31 | - Thanks Jonathan Hedley for [jsoup](http://jsoup.org/) under [MIT Licence](http://jsoup.org/license)
32 | - Please swing by my personal [Shaarli](https://shaarli.dimtion.fr)
33 |
34 | --------
35 |
36 | Software under [GPLv3](https://www.gnu.org/licenses/gpl.html)
37 |
--------------------------------------------------------------------------------
/Shaarlier.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 |
2 | ## To do ASAP
3 | - Improve automated testing
4 |
5 | ## Planned for future releases
6 | - Using new Shaarli API
7 | - Save link in a draft in case of an error
8 | - Feature: Handle redirection (E.g : shaarli.fr)
9 |
10 | ## Q&A
11 | - Try app on as many apps as possible
12 | - Try more device screen sizes
13 | - Try in low network conditions
14 | - Try on slow devices
15 |
16 | ## Long term evolutions (if requested)
17 | - See online Shaarli links
18 | - Save link later
19 | - Try to stay KISS
20 |
21 | [Any idea ?](https://github.com/dimtion/Shaarlier/issues)
22 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 |
4 | android {
5 | compileSdkVersion 33
6 |
7 | defaultConfig {
8 | applicationId "com.dimtion.shaarlier"
9 | minSdkVersion 15
10 | targetSdkVersion 33
11 | versionCode 33
12 | versionName "1.8.0"
13 |
14 | testApplicationId "com.dimtion.shaarliertest"
15 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled true
21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | namespace 'com.dimtion.shaarlier'
25 | }
26 |
27 | dependencies {
28 | implementation fileTree(dir: 'libs', include: ['*.jar'])
29 | implementation 'androidx.appcompat:appcompat:1.6.1'
30 | implementation 'org.jsoup:jsoup:1.11.3'
31 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
32 | implementation 'io.jsonwebtoken:jjwt:0.9.1'
33 | implementation 'me.xdrop:fuzzywuzzy:1.3.1'
34 |
35 | testImplementation 'junit:junit:4.13'
36 | implementation 'junit:junit:4.13'
37 | androidTestImplementation 'androidx.annotation:annotation:1.6.0'
38 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
39 | androidTestImplementation('androidx.test.uiautomator:uiautomator:2.3.0-alpha03')
40 | }
41 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in C:\Users\dimtion\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 | -keep public class org.jsoup.** {
19 | public *;
20 | }
21 |
22 | -keepattributes InnerClasses
23 |
24 | -keep class io.jsonwebtoken.** { *; }
25 | -keepnames class io.jsonwebtoken.* { *; }
26 | -keepnames interface io.jsonwebtoken.* { *; }
27 |
28 | -keep class org.bouncycastle.** { *; }
29 | -keepnames class org.bouncycastle.** { *; }
30 | -dontwarn org.bouncycastle.**
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/dimtion/shaarlier/network/NetworkUtilsTest.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.network;
2 |
3 | import static org.junit.Assert.assertFalse;
4 | import static org.junit.Assert.assertTrue;
5 |
6 | import com.dimtion.shaarlier.network.NetworkUtils;
7 |
8 | public class NetworkUtilsTest {
9 |
10 | @org.junit.Test
11 | public void isUrl() {
12 | assertTrue(NetworkUtils.isUrl("https://dimtion.fr/"));
13 | assertTrue(NetworkUtils.isUrl("http://dimtion.fr/"));
14 | assertTrue(NetworkUtils.isUrl("http://127.0.0.1/"));
15 |
16 | assertFalse(NetworkUtils.isUrl("http://"));
17 | assertFalse(NetworkUtils.isUrl("two words"));
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/dimtion/shaarlier/network/RestAPINetworkManagerTest.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.network;
2 |
3 | import com.dimtion.shaarlier.models.ShaarliAccount;
4 |
5 | import java.io.IOException;
6 |
7 | import static org.junit.Assert.assertNotNull;
8 |
9 | public class RestAPINetworkManagerTest {
10 | private ShaarliAccount mAccount;
11 | private RestAPINetworkManager mNetworkManager;
12 |
13 | @org.junit.Before
14 | public void setUp() throws Exception {
15 | this.mAccount = new ShaarliAccount();
16 | this.mAccount.setRestAPIKey("azerty");
17 | this.mNetworkManager = new RestAPINetworkManager(this.mAccount);
18 | }
19 |
20 | @org.junit.Test
21 | public void getJwt() throws IOException {
22 | assertNotNull(this.mAccount);
23 | String jwt = this.mNetworkManager.getJwt();
24 | assertNotNull(jwt);
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
14 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
74 |
77 |
78 |
83 |
86 |
87 |
88 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/activities/AccountsManagementActivity.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.activities;
2 |
3 | import android.content.Intent;
4 | import android.database.SQLException;
5 | import android.os.Bundle;
6 | import android.util.Log;
7 | import android.view.Menu;
8 | import android.view.MenuItem;
9 | import android.view.View;
10 | import android.widget.AdapterView;
11 | import android.widget.ArrayAdapter;
12 | import android.widget.ListView;
13 |
14 | import androidx.appcompat.app.AppCompatActivity;
15 |
16 | import com.dimtion.shaarlier.R;
17 | import com.dimtion.shaarlier.dao.AccountsSource;
18 | import com.dimtion.shaarlier.models.ShaarliAccount;
19 |
20 | import java.util.List;
21 |
22 |
23 | public class AccountsManagementActivity extends AppCompatActivity {
24 |
25 | @Override
26 | protected void onCreate(Bundle savedInstanceState) {
27 | super.onCreate(savedInstanceState);
28 | setContentView(R.layout.activity_accounts_management);
29 | }
30 |
31 | @Override
32 | protected void onResume() {
33 | super.onResume();
34 | final ListView accountsListView = findViewById(R.id.accountListView);
35 | AccountsSource accountsSource = new AccountsSource(getApplicationContext());
36 | try {
37 | accountsSource.rOpen();
38 | List accountsList = accountsSource.getAllAccounts();
39 | ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, accountsList);
40 |
41 | accountsListView.setAdapter(adapter);
42 |
43 | if (accountsList.isEmpty())
44 | findViewById(R.id.noAccountToShow).setVisibility(View.VISIBLE);
45 | else
46 | findViewById(R.id.noAccountToShow).setVisibility(View.GONE);
47 |
48 |
49 | } catch (SQLException e) {
50 | Log.e("DB_ERROR", e.toString());
51 | } finally {
52 | accountsSource.close();
53 | }
54 | accountsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
55 | @Override
56 | public void onItemClick(AdapterView> arg0, View arg1, int position, long arg3) {
57 | ShaarliAccount clickedAccount = (ShaarliAccount) accountsListView.getItemAtPosition(position);
58 |
59 | addOrEditAccount(clickedAccount);
60 | }
61 | });
62 | }
63 |
64 | private void addOrEditAccount(ShaarliAccount account) {
65 | Intent intent = new Intent(this, AddAccountActivity.class);
66 | if (account != null) {
67 | intent.putExtra("_id", account.getId());
68 | Log.w("EDIT ACCOUNT", account.getShortName());
69 | }
70 | startActivity(intent);
71 | }
72 | @Override
73 | public boolean onCreateOptionsMenu(Menu menu) {
74 | // Inflate the menu; this adds items to the action bar if it is present.
75 | getMenuInflater().inflate(R.menu.menu_accounts_management, menu);
76 | return true;
77 | }
78 |
79 | @Override
80 | public boolean onOptionsItemSelected(MenuItem item) {
81 | // Handle action bar item clicks here. The action bar will
82 | // automatically handle clicks on the Home/Up button, so long
83 | // as you specify a parent activity in AndroidManifest.xml.
84 | int id = item.getItemId();
85 |
86 | //noinspection SimplifiableIfStatement
87 | if (id == R.id.action_add) {
88 | addOrEditAccount(null);
89 | }
90 |
91 | return super.onOptionsItemSelected(item);
92 | }
93 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/activities/AddAccountActivity.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.activities;
2 |
3 | import android.app.Activity;
4 | import android.app.AlertDialog;
5 | import android.content.Context;
6 | import android.content.DialogInterface;
7 | import android.content.Intent;
8 | import android.content.SharedPreferences;
9 | import android.os.Bundle;
10 | import android.os.Handler;
11 | import android.os.Message;
12 | import android.os.Messenger;
13 | import android.util.Log;
14 | import android.view.Menu;
15 | import android.view.MenuItem;
16 | import android.view.View;
17 | import android.view.inputmethod.InputMethodManager;
18 | import android.widget.Button;
19 | import android.widget.CheckBox;
20 | import android.widget.EditText;
21 | import android.widget.Switch;
22 | import android.widget.Toast;
23 |
24 | import androidx.appcompat.app.AppCompatActivity;
25 |
26 | import com.dimtion.shaarlier.R;
27 | import com.dimtion.shaarlier.dao.AccountsSource;
28 | import com.dimtion.shaarlier.helper.DebugHelper;
29 | import com.dimtion.shaarlier.models.ShaarliAccount;
30 | import com.dimtion.shaarlier.network.NetworkUtils;
31 | import com.dimtion.shaarlier.services.NetworkService;
32 |
33 | import java.util.List;
34 |
35 |
36 | public class AddAccountActivity extends AppCompatActivity {
37 |
38 | private String urlShaarli;
39 | private String username;
40 | private String password;
41 | private String restAPIKey;
42 | private String basicAuthUsername;
43 | private String basicAuthPassword;
44 | private String shortName;
45 | private ShaarliAccount account;
46 | private Boolean isDefaultAccount;
47 | private Boolean isValidateCert;
48 | private int authMethod;
49 |
50 | private Boolean isEditing = false;
51 |
52 | @Override
53 | protected void onCreate(Bundle savedInstanceState) {
54 | super.onCreate(savedInstanceState);
55 | setContentView(R.layout.activity_add_account);
56 |
57 | Intent intent = getIntent();
58 | long accountId = intent.getLongExtra("_id", -1);
59 |
60 | if (accountId != -1) {
61 | isEditing = true;
62 | AccountsSource accountsSource = new AccountsSource(getApplicationContext());
63 | try {
64 | account = accountsSource.getShaarliAccountById(accountId);
65 | } catch (Exception e) { // Editing an account that does not exist, creating a new one
66 | account = null;
67 | }
68 | fillFields();
69 | } else {
70 | AccountsSource source = new AccountsSource(getApplicationContext());
71 | source.rOpen();
72 | List allAccounts = source.getAllAccounts();
73 | if (allAccounts.isEmpty()) { // If it is the first account created
74 | CheckBox defaultCheck = findViewById(R.id.defaultAccountCheck);
75 | defaultCheck.setChecked(true);
76 | defaultCheck.setEnabled(false);
77 | }
78 | }
79 | }
80 |
81 | /**
82 | * Fill the fields with the selected account when editing a new account
83 | */
84 | private void fillFields() {
85 | // Get the user inputs :
86 | ((EditText) findViewById(R.id.urlShaarliView)).setText(account.getUrlShaarli());
87 | ((EditText) findViewById(R.id.usernameView)).setText(account.getUsername());
88 | ((EditText) findViewById(R.id.passwordView)).setText(account.getPassword());
89 | ((EditText) findViewById(R.id.shortNameView)).setText(account.getShortName());
90 | ((EditText) findViewById(R.id.restapiView)).setText(account.getRestAPIKey());
91 |
92 | ((Switch) findViewById(R.id.passwordAuthCheckbox)).setChecked(
93 | !(account.getRestAPIKey().length() > 0)
94 | );
95 | togglePasswordAuth(findViewById(R.id.passwordAuthCheckbox));
96 |
97 | if (!"".equals(account.getBasicAuthUsername())) {
98 | ((EditText) findViewById(R.id.basicUsernameView)).setText(account.getBasicAuthUsername());
99 | ((EditText) findViewById(R.id.basicPasswordView)).setText(account.getBasicAuthPassword());
100 | ((Switch) findViewById(R.id.basicAuthSwitch)).setChecked(true);
101 | enableBasicAuth(findViewById(R.id.basicAuthSwitch));
102 | }
103 |
104 | // default account?
105 | SharedPreferences prefs = getSharedPreferences(getString(R.string.params), MODE_PRIVATE);
106 | this.isDefaultAccount = (prefs.getLong(getString(R.string.p_default_account), -1) == account.getId());
107 | ((CheckBox) findViewById(R.id.defaultAccountCheck)).setChecked(this.isDefaultAccount);
108 |
109 | findViewById(R.id.deleteAccountButton).setVisibility(View.VISIBLE);
110 | }
111 |
112 | /**
113 | * Action which handle the press on the try and save button
114 | *
115 | * @param view: needed for binding with interface actions
116 | */
117 | public void tryAndSaveAction(View view) {
118 | hideKeyboard();
119 | findViewById(R.id.tryingConfSpinner).setVisibility(View.VISIBLE);
120 | findViewById(R.id.tryConfButton).setVisibility(View.GONE);
121 |
122 | // Get the user inputs:
123 | final String urlShaarliInput = ((EditText) findViewById(R.id.urlShaarliView)).getText().toString();
124 | this.username = ((EditText) findViewById(R.id.usernameView)).getText().toString();
125 | this.password = ((EditText) findViewById(R.id.passwordView)).getText().toString();
126 | this.restAPIKey = ((EditText) findViewById(R.id.restapiView)).getText().toString();
127 | if (((Switch) findViewById(R.id.passwordAuthCheckbox)).isChecked()) {
128 | this.authMethod = ShaarliAccount.AUTH_METHOD_PASSWORD;
129 | } else {
130 | this.authMethod = ShaarliAccount.AUTH_METHOD_RESTAPI;
131 | }
132 | if (((Switch) findViewById(R.id.basicAuthSwitch)).isChecked()) {
133 | this.basicAuthUsername = ((EditText) findViewById(R.id.basicUsernameView)).getText().toString();
134 | this.basicAuthPassword = ((EditText) findViewById(R.id.basicPasswordView)).getText().toString();
135 | } else {
136 | this.basicAuthUsername = "";
137 | this.basicAuthPassword = "";
138 | }
139 | this.shortName = ((EditText) findViewById(R.id.shortNameView)).getText().toString();
140 | this.isDefaultAccount = ((CheckBox) findViewById(R.id.defaultAccountCheck)).isChecked();
141 | this.isValidateCert = !((CheckBox) findViewById(R.id.disableCertValidation)).isChecked();
142 |
143 | this.urlShaarli = NetworkUtils.toUrl(urlShaarliInput);
144 |
145 | ((EditText) findViewById(R.id.urlShaarliView)).setText(this.urlShaarli); // Update the view
146 |
147 | // Depending on the
148 | // Create a fake account:
149 | ShaarliAccount accountToTest = new ShaarliAccount();
150 | accountToTest.setUrlShaarli(this.urlShaarli);
151 | accountToTest.setUsername(this.username);
152 | accountToTest.setPassword(this.password);
153 | accountToTest.setRestAPIKey(this.restAPIKey);
154 | accountToTest.setValidateCert(this.isValidateCert);
155 | accountToTest.setBasicAuthUsername(this.basicAuthUsername);
156 | accountToTest.setBasicAuthPassword(this.basicAuthPassword);
157 | accountToTest.setAuthMethod(this.authMethod);
158 |
159 | // Try the configuration
160 | Intent i = new Intent(this, NetworkService.class);
161 | i.putExtra("action", NetworkService.INTENT_CHECK);
162 | i.putExtra("account", accountToTest);
163 | i.putExtra(NetworkService.EXTRA_MESSENGER, new Messenger(new networkHandler(this, accountToTest)));
164 | startService(i);
165 | }
166 |
167 | /**
168 | * Handle the action of deletion: show a confirmation dialog then delete (if wanted)
169 | * @param view : The view needed for handling interface actions
170 | */
171 | public void deleteAccountAction(View view) {
172 | // Show dialog to confirm deletion
173 | AlertDialog.Builder builder = new AlertDialog.Builder(this);
174 | builder.setMessage(getString(R.string.text_confirm_deletion_account));
175 | builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
176 | @Override
177 | public void onClick(DialogInterface dialog, int which) {
178 | deleteAccount();
179 | finish();
180 | }
181 | });
182 |
183 | builder.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
184 | public void onClick(DialogInterface dialog, int which) {
185 | // Just dismiss the dialog
186 | }
187 | });
188 | builder.show();
189 | }
190 |
191 | /**
192 | * Delete the selected account from the database
193 | */
194 | private void deleteAccount() {
195 | AccountsSource source = new AccountsSource(getApplicationContext());
196 | source.wOpen();
197 | source.deleteAccount(this.account);
198 | source.close();
199 | }
200 |
201 | @Override
202 | public boolean onCreateOptionsMenu(Menu menu) {
203 | // Inflate the menu; this adds items to the action bar if it is present.
204 | getMenuInflater().inflate(R.menu.menu_add_account, menu);
205 | return true;
206 | }
207 |
208 | @Override
209 | public boolean onOptionsItemSelected(MenuItem item) {
210 | // Handle action bar item clicks here. The action bar will
211 | // automatically handle clicks on the Home/Up button, so long
212 | // as you specify a parent activity in AndroidManifest.xml.
213 | int id = item.getItemId();
214 |
215 | //noinspection SimplifiableIfStatement
216 | if (id == R.id.action_validate) {
217 | tryAndSaveAction(findViewById(id));
218 | }
219 |
220 | return super.onOptionsItemSelected(item);
221 | }
222 |
223 | /**
224 | * Obviously hide the keyboard
225 | * From: http://stackoverflow.com/a/7696791/1582589
226 | */
227 | private void hideKeyboard() {
228 | // Check if no view has focus:
229 | View view = this.getCurrentFocus();
230 | if (view != null) {
231 | InputMethodManager inputManager = (InputMethodManager) this.getSystemService(Context.INPUT_METHOD_SERVICE);
232 | inputManager.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
233 | }
234 | }
235 |
236 | public void enableBasicAuth(View toggle) {
237 | boolean checked = ((Switch) toggle).isChecked();
238 | findViewById(R.id.basicUsernameView).setEnabled(checked);
239 | findViewById(R.id.basicPasswordView).setEnabled(checked);
240 | findViewById(R.id.basicUsernameView).setVisibility(checked ? View.VISIBLE : View.GONE);
241 | findViewById(R.id.basicPasswordView).setVisibility(checked ? View.VISIBLE : View.GONE);
242 | }
243 |
244 | public void togglePasswordAuth(View toggle) {
245 | boolean checked = ((Switch) toggle).isChecked();
246 | findViewById(R.id.usernameView).setEnabled(checked);
247 | findViewById(R.id.usernameView).setVisibility(checked ? View.VISIBLE : View.GONE);
248 |
249 | findViewById(R.id.passwordView).setEnabled(checked);
250 | findViewById(R.id.passwordView).setVisibility(checked ? View.VISIBLE : View.GONE);
251 |
252 | findViewById(R.id.basicAuthSwitch).setEnabled(checked);
253 | findViewById(R.id.basicAuthSwitch).setVisibility(checked ? View.VISIBLE : View.GONE);
254 |
255 | findViewById(R.id.restapiView).setEnabled(!checked);
256 | findViewById(R.id.restapiView).setVisibility(!checked ? View.VISIBLE : View.GONE);
257 | }
258 |
259 | /**
260 | * Save a new account into the database,
261 | * should be called only if the account was verified.
262 | */
263 | private void saveAccount() {
264 | AccountsSource accountsSource = new AccountsSource(getApplicationContext());
265 | accountsSource.wOpen();
266 | // Since we do not yet save in the database the auth method, we rely on ShaarliAccount.AUTH_METHOD_AUTO
267 | // for publishing.
268 | // TODO: add to database saved auth method
269 | switch (authMethod) {
270 | case ShaarliAccount.AUTH_METHOD_PASSWORD:
271 | this.restAPIKey = "";
272 | break;
273 | case ShaarliAccount.AUTH_METHOD_RESTAPI:
274 | this.username = "";
275 | this.password = "";
276 | break;
277 | }
278 |
279 | try {
280 | if (isEditing) { // Only update the database
281 | account.setUrlShaarli(this.urlShaarli);
282 | account.setUsername(this.username);
283 | account.setPassword(this.password);
284 | account.setBasicAuthUsername(this.basicAuthUsername);
285 | account.setBasicAuthPassword(this.basicAuthPassword);
286 | account.setShortName(this.shortName);
287 | account.setValidateCert(this.isValidateCert);
288 | account.setRestAPIKey(this.restAPIKey);
289 | accountsSource.editAccount(account);
290 | } else {
291 | this.account = accountsSource.createAccount(
292 | this.urlShaarli,
293 | this.username,
294 | this.password,
295 | this.basicAuthUsername,
296 | this.basicAuthPassword,
297 | this.shortName,
298 | this.isValidateCert,
299 | this.restAPIKey
300 | );
301 | }
302 | } catch (Exception e) {
303 | Log.e("ENCRYPTION ERROR", e.getMessage());
304 | } finally {
305 | accountsSource.close();
306 | }
307 |
308 | // Set the default account if needed
309 | if (this.isDefaultAccount) {
310 | SharedPreferences prefs = getSharedPreferences(getString(R.string.params), MODE_PRIVATE);
311 | SharedPreferences.Editor editor = prefs.edit();
312 |
313 | editor.putLong(getString(R.string.p_default_account), this.account.getId());
314 | editor.apply();
315 | }
316 | }
317 |
318 | private class networkHandler extends Handler {
319 | private final Activity mParent;
320 | private final ShaarliAccount mAccount;
321 |
322 | public networkHandler(Activity parent, ShaarliAccount account) {
323 | this.mParent = parent;
324 | this.mAccount = account;
325 | }
326 |
327 | /**
328 | * Handle the arrival of a message coming from the network service.
329 | *
330 | * @param msg the message given by the service
331 | */
332 | @Override
333 | public void handleMessage(Message msg) {
334 | findViewById(R.id.tryConfButton).setVisibility(View.VISIBLE);
335 | findViewById(R.id.tryingConfSpinner).setVisibility(View.GONE);
336 |
337 | // Show the returned error
338 | switch (msg.arg1) {
339 | case NetworkService.NO_ERROR:
340 | Toast.makeText(getApplicationContext(), R.string.success_test, Toast.LENGTH_LONG).show();
341 | saveAccount();
342 | finish();
343 | break;
344 | case NetworkService.NETWORK_ERROR:
345 | ((EditText) findViewById(R.id.urlShaarliView)).setError(getString(R.string.error_connecting));
346 | enableSendReport((Exception) msg.obj);
347 | break;
348 | case NetworkService.TOKEN_ERROR:
349 | ((EditText) findViewById(R.id.urlShaarliView)).setError(getString(R.string.error_parsing_token));
350 | enableSendReport(new Exception("TOKEN ERROR"));
351 | break;
352 | case NetworkService.LOGIN_ERROR:
353 | ((EditText) findViewById(R.id.usernameView)).setError(getString(R.string.error_login));
354 | ((EditText) findViewById(R.id.passwordView)).setError(getString(R.string.error_login));
355 | ((EditText) findViewById(R.id.restapiView)).setError(getString(R.string.error_login));
356 | enableSendReport(new Exception("LOGIN ERROR"));
357 | break;
358 | default:
359 | ((EditText) findViewById(R.id.urlShaarliView)).setError(getString(R.string.error_unknown));
360 | Toast.makeText(getApplicationContext(), R.string.error_unknown, Toast.LENGTH_LONG).show();
361 | enableSendReport(new Exception("UNKNOWN ERROR"));
362 | break;
363 | }
364 | }
365 |
366 | private void enableSendReport(final Exception error) {
367 | Button reportButton = findViewById(R.id.sendReportButton);
368 | reportButton.setVisibility(View.VISIBLE);
369 | reportButton.setOnClickListener(new View.OnClickListener() {
370 | @Override
371 | public void onClick(View v) {
372 | AlertDialog.Builder builder = new AlertDialog.Builder(mParent);
373 |
374 | builder.setMessage(R.string.report_issue).setTitle("REPORT - Shaarlier");
375 |
376 | String extra = "Url Shaarli: " + urlShaarli + "\n";
377 | extra += "Auth method id: " + mAccount.getAuthMethod() + "\n";
378 | extra += "Validate cert: " + mAccount.isValidateCert() + "\n";
379 | extra += "Basic auth: " + ((mAccount.getBasicAuthUsername().length() > 0) || (mAccount.getBasicAuthPassword().length() > 0)) + "\n";
380 |
381 | final String finalExtra = extra;
382 | builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
383 | public void onClick(DialogInterface dialog, int id) {
384 | DebugHelper.sendMailDev(
385 | mParent,
386 | "REPORT - Shaarlier",
387 | DebugHelper.generateReport(error, mParent, finalExtra)
388 | );
389 | }
390 | });
391 | builder.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
392 | public void onClick(DialogInterface dialog, int id) {
393 | // User cancelled the dialog
394 | }
395 | });
396 | AlertDialog dialog = builder.create();
397 | dialog.show();
398 | }
399 | });
400 | }
401 | }
402 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/activities/HttpSchemeHandlerActivity.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.activities;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.net.Uri;
6 | import android.os.Bundle;
7 | import android.widget.Toast;
8 |
9 | import com.dimtion.shaarlier.R;
10 |
11 | public class HttpSchemeHandlerActivity extends Activity {
12 |
13 | @Override
14 | protected void onCreate(Bundle savedInstanceState) {
15 | super.onCreate(savedInstanceState);
16 |
17 | Uri data = getIntent().getData();
18 | if(data != null) {
19 | String url = data.toString();
20 |
21 | Intent addActivityIntent = new Intent(this, ShareActivity.class);
22 | addActivityIntent.setAction(Intent.ACTION_SEND);
23 | addActivityIntent.setType("text/plain");
24 | addActivityIntent.putExtra(Intent.EXTRA_TEXT, url);
25 | startActivity(addActivityIntent);
26 | } else {
27 | Toast.makeText(getApplicationContext(), R.string.add_not_handle, Toast.LENGTH_SHORT).show();
28 | }
29 |
30 | finish();
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/activities/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.activities;
2 |
3 | import android.app.AlertDialog;
4 | import android.content.ComponentName;
5 | import android.content.DialogInterface;
6 | import android.content.Intent;
7 | import android.content.SharedPreferences;
8 | import android.content.pm.PackageManager;
9 | import android.net.Uri;
10 | import android.os.Build;
11 | import android.os.Bundle;
12 | import android.text.InputType;
13 | import android.text.method.LinkMovementMethod;
14 | import android.view.Menu;
15 | import android.view.MenuItem;
16 | import android.view.View;
17 | import android.view.ViewGroup;
18 | import android.widget.Button;
19 | import android.widget.CheckBox;
20 | import android.widget.EditText;
21 | import android.widget.LinearLayout;
22 | import android.widget.TextView;
23 |
24 | import androidx.appcompat.app.AppCompatActivity;
25 |
26 | import com.dimtion.shaarlier.R;
27 | import com.dimtion.shaarlier.dao.AccountsSource;
28 | import com.dimtion.shaarlier.models.ShaarliAccount;
29 |
30 | import java.util.List;
31 |
32 |
33 | public class MainActivity extends AppCompatActivity {
34 | private boolean m_isNoAccount;
35 |
36 | @Override
37 | protected void onCreate(Bundle savedInstanceState) {
38 | super.onCreate(savedInstanceState);
39 | setContentView(R.layout.activity_main);
40 |
41 | // Make links clickable :
42 | ((TextView) findViewById(R.id.about_details)).setMovementMethod(LinkMovementMethod.getInstance());
43 |
44 | loadSettings();
45 |
46 | // Load custom design :
47 | TextView textVersion = findViewById(R.id.text_version);
48 | try {
49 | String versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
50 | textVersion.setText(String.format(getString(R.string.version), versionName));
51 |
52 | } catch (PackageManager.NameNotFoundException e) {
53 | textVersion.setText(getText(R.string.text_version));
54 | }
55 | }
56 |
57 | @Override
58 | public void onResume() {
59 | super.onResume();
60 | AccountsSource accountsSource = new AccountsSource(getApplicationContext());
61 | accountsSource.rOpen();
62 | m_isNoAccount = accountsSource.getAllAccounts().isEmpty();
63 | accountsSource.close();
64 |
65 | Button manageAccountsButton = findViewById(R.id.button_manage_accounts);
66 | if (m_isNoAccount) {
67 | manageAccountsButton.setText(R.string.add_account);
68 | } else {
69 | manageAccountsButton.setText(R.string.button_manage_accounts);
70 | }
71 | }
72 |
73 | @Override
74 | public void onPause() {
75 | super.onPause();
76 | saveSettings();
77 | }
78 |
79 | private void saveSettings() {
80 | // Get user inputs :
81 | boolean isPrivate = ((CheckBox) findViewById(R.id.default_private)).isChecked();
82 | boolean isShareDialog = ((CheckBox) findViewById(R.id.show_share_dialog)).isChecked();
83 | boolean isAutoTitle = ((CheckBox) findViewById(R.id.auto_load_title)).isChecked();
84 | boolean isAutoDescription = ((CheckBox) findViewById(R.id.auto_load_description)).isChecked();
85 | boolean isHandlingHttpScheme = ((CheckBox) findViewById(R.id.handle_http_scheme)).isChecked();
86 | boolean useShaarli2twitter = ((CheckBox) findViewById(R.id.handle_twitter_plugin)).isChecked();
87 | boolean useShaarli2mastodon = ((CheckBox) findViewById(R.id.handle_mastodon_plugin)).isChecked();
88 |
89 | // Save data :
90 | SharedPreferences pref = getSharedPreferences(getString(R.string.params), MODE_PRIVATE);
91 | SharedPreferences.Editor editor = pref.edit();
92 | editor.putBoolean(getString(R.string.p_default_private), isPrivate)
93 | .putBoolean(getString(R.string.p_show_share_dialog), isShareDialog)
94 | .putBoolean(getString(R.string.p_auto_title), isAutoTitle)
95 | .putBoolean(getString(R.string.p_auto_description), isAutoDescription)
96 | .putBoolean(getString(R.string.p_shaarli2twitter), useShaarli2twitter)
97 | .putBoolean(getString(R.string.p_shaarli2mastodon), useShaarli2mastodon)
98 | .apply();
99 |
100 | setHandleHttpScheme(isHandlingHttpScheme);
101 | }
102 |
103 | public void openAccountsManager(View view) {
104 | Intent intent;
105 | if (m_isNoAccount) {
106 | intent = new Intent(this, AddAccountActivity.class);
107 | } else {
108 | intent = new Intent(this, AccountsManagementActivity.class);
109 |
110 | }
111 | startActivity(intent);
112 |
113 | }
114 |
115 | private void loadSettings() {
116 | // Retrieve user previous settings
117 | SharedPreferences pref = getSharedPreferences(getString(R.string.params), MODE_PRIVATE);
118 | // updateSettingsFromUpdate(pref);
119 |
120 | boolean prv = pref.getBoolean(getString(R.string.p_default_private), false);
121 | boolean sherDial = pref.getBoolean(getString(R.string.p_show_share_dialog), true);
122 | boolean isAutoTitle = pref.getBoolean(getString(R.string.p_auto_title), true);
123 | boolean isAutoDescription = pref.getBoolean(getString(R.string.p_auto_description), false);
124 | boolean isHandlingHttpScheme = isHandlingHttpScheme();
125 | boolean isShaarli2twitter = pref.getBoolean(getString(R.string.p_shaarli2twitter), false);
126 | boolean isShaarli2mastodon = pref.getBoolean(getString(R.string.p_shaarli2mastodon), false);
127 |
128 | // Retrieve interface :
129 | CheckBox privateCheck = findViewById(R.id.default_private);
130 | CheckBox shareDialogCheck = findViewById(R.id.show_share_dialog);
131 | CheckBox autoTitleCheck = findViewById(R.id.auto_load_title);
132 | CheckBox autoDescriptionCheck = findViewById(R.id.auto_load_description);
133 | CheckBox handleHttpSchemeCheck = findViewById(R.id.handle_http_scheme);
134 | CheckBox shaarli2twitter = findViewById(R.id.handle_twitter_plugin);
135 | CheckBox shaarli2mastodon = findViewById(R.id.handle_mastodon_plugin);
136 |
137 | // Display user previous settings :
138 | privateCheck.setChecked(prv);
139 | autoTitleCheck.setChecked(isAutoTitle);
140 | autoDescriptionCheck.setChecked(isAutoDescription);
141 | handleHttpSchemeCheck.setChecked(isHandlingHttpScheme);
142 | shareDialogCheck.setChecked(sherDial);
143 | shaarli2twitter.setChecked(isShaarli2twitter);
144 | shaarli2mastodon.setChecked(isShaarli2mastodon);
145 | }
146 |
147 | @Override
148 | public boolean onCreateOptionsMenu(Menu menu) {
149 | // Inflate the menu; this adds items to the action bar if it is present.
150 | getMenuInflater().inflate(R.menu.menu_main, menu);
151 | if (m_isNoAccount) {
152 | menu.findItem(R.id.action_share).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
153 | } else {
154 | menu.findItem(R.id.action_share).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
155 | }
156 |
157 | return true;
158 | }
159 |
160 | @Override
161 | public boolean onOptionsItemSelected(MenuItem item) {
162 | int id = item.getItemId();
163 |
164 | if (id == R.id.action_share) {
165 | shareDialog();
166 | } else if (id == R.id.action_open_shaarli) {
167 | openShaarliDialog();
168 | } else {
169 | return true;
170 | }
171 | return super.onOptionsItemSelected(item);
172 | }
173 |
174 | private void shareDialog() {
175 | AlertDialog.Builder alert = new AlertDialog.Builder(this);
176 |
177 | alert.setTitle(getString(R.string.share));
178 |
179 | // TODO: move this to a xml file:
180 | final LinearLayout layout = new LinearLayout(this);
181 |
182 | final TextView textView = new TextView(this);
183 | if (Build.VERSION.SDK_INT < 23) {
184 | //noinspection deprecation
185 | textView.setTextAppearance(this, android.R.style.TextAppearance_Medium);
186 | } else {
187 | textView.setTextAppearance(android.R.style.TextAppearance_Medium);
188 | }
189 | textView.setText(getText(R.string.text_new_url));
190 |
191 | // Set an EditText view to get user input
192 | final EditText input = new EditText(this);
193 | input.setInputType(InputType.TYPE_TEXT_VARIATION_URI);
194 | input.setHint(getText(R.string.hint_new_url));
195 |
196 | layout.setOrientation(LinearLayout.VERTICAL);
197 | layout.setPadding(10, 10, 20, 20);
198 |
199 | layout.addView(textView);
200 | layout.addView(input);
201 | alert.setView(layout);
202 |
203 | alert.setPositiveButton(getString(android.R.string.yes), new DialogInterface.OnClickListener() {
204 | public void onClick(DialogInterface dialog, int whichButton) {
205 | String value = input.getText().toString();
206 | Intent intent = new Intent(getBaseContext(), ShareActivity.class);
207 | intent.setAction(Intent.ACTION_SEND);
208 | intent.setType("text/plain");
209 | intent.putExtra(Intent.EXTRA_TEXT, value);
210 | startActivity(intent);
211 | }
212 | });
213 |
214 | alert.setNegativeButton(getString(android.R.string.no), new DialogInterface.OnClickListener() {
215 | public void onClick(DialogInterface dialog, int whichButton) {
216 | // Canceled.
217 | }
218 | });
219 |
220 | alert.show();
221 | }
222 |
223 | private void openShaarliDialog() {
224 | AccountsSource accountsSource = new AccountsSource(this);
225 | accountsSource.rOpen();
226 | final List accounts = accountsSource.getAllAccounts();
227 | accountsSource.close();
228 |
229 | if (accounts == null || accounts.size() < 1) {
230 | return;
231 | }
232 | if (accounts.size() == 1) {
233 | Intent browserIntent = new Intent(
234 | Intent.ACTION_VIEW,
235 | Uri.parse(accounts.get(0).getUrlShaarli())
236 | );
237 | startActivity(browserIntent);
238 | }
239 |
240 | final AlertDialog.Builder alert = new AlertDialog.Builder(this);
241 |
242 | alert.setTitle(getString(R.string.action_open_shaarli));
243 |
244 | // TODO: move this to a xml file:
245 | final LinearLayout layout = new LinearLayout(this);
246 | for (final ShaarliAccount account : accounts) {
247 | Button b = new Button(this);
248 | b.setLayoutParams(new ViewGroup.LayoutParams(
249 | ViewGroup.LayoutParams.MATCH_PARENT,
250 | ViewGroup.LayoutParams.WRAP_CONTENT)
251 | );
252 | b.setText(account.getShortName());
253 | b.setOnClickListener(new View.OnClickListener() {
254 | @Override
255 | public void onClick(View v) {
256 | Intent browserIntent = new Intent(
257 | Intent.ACTION_VIEW,
258 | Uri.parse(account.getUrlShaarli())
259 | );
260 | startActivity(browserIntent);
261 | }
262 | });
263 |
264 | layout.addView(b);
265 | }
266 |
267 | layout.setOrientation(LinearLayout.VERTICAL);
268 | layout.setPadding(10, 10, 20, 20);
269 |
270 | alert.setView(layout);
271 |
272 | alert.setNegativeButton(getString(android.R.string.no), new DialogInterface.OnClickListener() {
273 | public void onClick(DialogInterface dialog, int whichButton) {
274 | // Canceled.
275 | }
276 | });
277 |
278 | alert.show();
279 | }
280 |
281 | private boolean isHandlingHttpScheme() {
282 | return getPackageManager().getComponentEnabledSetting(getHttpSchemeHandlingComponent())
283 | == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
284 | }
285 |
286 | private void setHandleHttpScheme(boolean handleHttpScheme) {
287 | if(handleHttpScheme == isHandlingHttpScheme()) return;
288 |
289 | int flag = (handleHttpScheme ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
290 | : PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
291 |
292 | getPackageManager().setComponentEnabledSetting(
293 | getHttpSchemeHandlingComponent(), flag, PackageManager.DONT_KILL_APP);
294 | }
295 |
296 | private ComponentName getHttpSchemeHandlingComponent() {
297 | return new ComponentName(this, HttpSchemeHandlerActivity.class);
298 | }
299 |
300 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/activities/ShareActivity.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.activities;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.os.Handler;
6 | import android.os.Message;
7 | import android.os.Messenger;
8 | import android.text.Editable;
9 | import android.text.TextWatcher;
10 | import android.util.Log;
11 | import android.view.Menu;
12 | import android.view.MenuItem;
13 | import android.view.View;
14 | import android.widget.AdapterView;
15 | import android.widget.ArrayAdapter;
16 | import android.widget.Checkable;
17 | import android.widget.EditText;
18 | import android.widget.MultiAutoCompleteTextView;
19 | import android.widget.Spinner;
20 | import android.widget.Switch;
21 | import android.widget.Toast;
22 |
23 | import androidx.annotation.NonNull;
24 | import androidx.appcompat.app.AppCompatActivity;
25 |
26 | import com.dimtion.shaarlier.R;
27 | import com.dimtion.shaarlier.dao.AccountsSource;
28 | import com.dimtion.shaarlier.exceptions.UnsupportedIntent;
29 | import com.dimtion.shaarlier.helper.AutoCompleteWrapper;
30 | import com.dimtion.shaarlier.helper.ShareIntentParser;
31 | import com.dimtion.shaarlier.models.Link;
32 | import com.dimtion.shaarlier.models.ShaarliAccount;
33 | import com.dimtion.shaarlier.network.NetworkUtils;
34 | import com.dimtion.shaarlier.services.NetworkService;
35 | import com.dimtion.shaarlier.utils.UserPreferences;
36 |
37 | import java.util.ArrayList;
38 | import java.util.Collections;
39 | import java.util.List;
40 | import java.util.Objects;
41 |
42 | public class ShareActivity extends AppCompatActivity {
43 |
44 | static final int LOADER_TITLE = 0;
45 | static final int LOADER_DESCRIPTION = 1;
46 | static final int LOADER_PREFETCH = 2;
47 | private static final String LOGGER_NAME = ShareActivity.class.getSimpleName();
48 | private Link defaults;
49 | private UserPreferences userPrefs;
50 | private List accounts;
51 |
52 | private boolean isLoadingTitle = false;
53 | private boolean isLoadingDescription = false;
54 | private boolean isPrefetching = false;
55 |
56 | private boolean isNotNewLink = false;
57 | private Menu menu;
58 |
59 | @Override
60 | public boolean onCreateOptionsMenu(final Menu menu) {
61 | getMenuInflater().inflate(R.menu.menu_share, menu);
62 | this.menu = menu;
63 | return true;
64 | }
65 |
66 | @Override
67 | protected void onCreate(final Bundle savedInstanceState) {
68 | userPrefs = UserPreferences.load(this);
69 | if (userPrefs.isOpenDialog()) {
70 | setTheme(R.style.AppTheme);
71 | }
72 |
73 | super.onCreate(savedInstanceState);
74 |
75 | final List loadedAccounts = loadAccounts();
76 | if (loadedAccounts.isEmpty()) {
77 | Log.w(LOGGER_NAME, "No account configured, starting MainActivity");
78 | startActivity(new Intent(this, MainActivity.class));
79 | return;
80 | }
81 |
82 | this.accounts = loadedAccounts;
83 | final ShaarliAccount selectedAccount = loadedAccounts.get(0);
84 |
85 | final Intent intent = getIntent();
86 | final ShareIntentParser intentParser = new ShareIntentParser(selectedAccount, userPrefs);
87 | try {
88 | defaults = intentParser.parse(this, intent);
89 | } catch (final UnsupportedIntent e) {
90 | Toast.makeText(getApplicationContext(), R.string.add_not_handle, Toast.LENGTH_SHORT).show();
91 | finish();
92 | return;
93 | }
94 |
95 | if (userPrefs.isOpenDialog()) {
96 | openDialog(defaults, userPrefs);
97 | } else {
98 | autoLoadTitleAndDescription(defaults);
99 | sendLink(defaults);
100 | finish();
101 | }
102 | }
103 |
104 | /**
105 | * Load user accounts and set the default one as the first of the list
106 | */
107 | private List loadAccounts() {
108 | final AccountsSource accountsSource = new AccountsSource(this);
109 | ShaarliAccount defaultAccount = null;
110 | List accounts = new ArrayList<>();
111 | accountsSource.rOpen();
112 | try {
113 | accounts = accountsSource.getAllAccounts();
114 | defaultAccount = accountsSource.getDefaultAccount();
115 | } catch (final Exception e) {
116 | Log.e(LOGGER_NAME, "Error while loading account sources");
117 | e.printStackTrace();
118 | } finally {
119 | accountsSource.close();
120 | }
121 |
122 | if (Objects.isNull(defaultAccount)) {
123 | return accounts;
124 | }
125 |
126 | // Put the default account first in the list
127 | int indexSelectedAccount = 0;
128 | for (final ShaarliAccount account : accounts) {
129 | if (account.getId() == defaultAccount.getId()) {
130 | break;
131 | }
132 | indexSelectedAccount++;
133 | }
134 | Collections.swap(accounts, indexSelectedAccount, 0);
135 |
136 | return accounts;
137 | }
138 |
139 | /**
140 | * Open a dialog for the user to change the description of the share
141 | */
142 | private void openDialog(final Link defaults, final UserPreferences userPrefs) {
143 | setContentView(R.layout.activity_share);
144 |
145 | initAccountSpinner();
146 |
147 | if (NetworkUtils.isUrl(defaults.getUrl())) {
148 | prefetchLink(defaults);
149 | autoLoadTitleAndDescription(defaults);
150 | updateLoadersVisibility();
151 | }
152 |
153 | ((EditText) findViewById(R.id.url)).setText(defaults.getUrl());
154 |
155 | final MultiAutoCompleteTextView textView = findViewById(R.id.tags);
156 | ((EditText) findViewById(R.id.tags)).setText(defaults.getTags());
157 | new AutoCompleteWrapper(textView, this);
158 |
159 | ((Checkable) findViewById(R.id.private_share)).setChecked(defaults.isPrivate());
160 |
161 | { // Init tweet if necessary
162 | final Switch tweetCheckBox = findViewById(R.id.tweet);
163 | tweetCheckBox.setChecked(userPrefs.isTweet());
164 | tweetCheckBox.setVisibility(userPrefs.isTweet() ? View.VISIBLE : View.GONE);
165 | }
166 |
167 | { // Init toot if necessary
168 | final Switch tootCheckBox = findViewById(R.id.toot);
169 | tootCheckBox.setChecked(userPrefs.isToot());
170 | tootCheckBox.setVisibility(userPrefs.isTweet() ? View.VISIBLE : View.GONE);
171 | }
172 | }
173 |
174 | /**
175 | * Initiate a network handler to prefetch link information on Shaarli.
176 | * This can be used to know if a link was already saved, and then retrieve
177 | * its data.
178 | *
179 | * TODO: use this prefetching in order to save one network request when sharing
180 | * @param defaults defaults values
181 | */
182 | private void prefetchLink(@NonNull Link defaults) {
183 | final Intent networkIntent = new Intent(this, NetworkService.class);
184 | networkIntent.putExtra("action", NetworkService.INTENT_PREFETCH);
185 | networkIntent.putExtra("link", defaults);
186 | networkIntent.putExtra(NetworkService.EXTRA_MESSENGER, new Messenger(new NetworkHandler()));
187 |
188 | isPrefetching = true;
189 | startService(networkIntent);
190 | }
191 |
192 | private void initAccountSpinner() {
193 | final Spinner accountSpinner = this.findViewById(R.id.chooseAccount);
194 | ArrayAdapter adapter = new ArrayAdapter<>(this, R.layout.tags_list, accounts);
195 | accountSpinner.setAdapter(adapter);
196 | if (accountSpinner.getCount() < 2) {
197 | accountSpinner.setVisibility(View.GONE);
198 | }
199 | AccountSpinnerListener listener = new AccountSpinnerListener();
200 | accountSpinner.setOnItemSelectedListener(listener);
201 | }
202 |
203 | /**
204 | * Load everything from the interface and share the link
205 | */
206 | private void saveAndShare() {
207 | sendLink(linkFromUserInput());
208 | finish();
209 | }
210 |
211 | private void autoLoadTitleAndDescription(Link defaults) {
212 | // Don't use network resources if not needed
213 | if (!userPrefs.isAutoTitle() && !userPrefs.isAutoDescription()) {
214 | return;
215 | }
216 | // Launch intent to retrieve the title and the description
217 | final Intent networkIntent = new Intent(this, NetworkService.class);
218 | networkIntent.putExtra("action", NetworkService.INTENT_RETRIEVE_TITLE_AND_DESCRIPTION);
219 | networkIntent.putExtra("url", defaults.getUrl());
220 | networkIntent.putExtra("autoTitle", userPrefs.isAutoTitle());
221 | networkIntent.putExtra("autoDescription", userPrefs.isAutoDescription());
222 | networkIntent.putExtra(NetworkService.EXTRA_MESSENGER, new Messenger(new NetworkHandler()));
223 |
224 | isLoadingTitle = true;
225 | isLoadingDescription = true;
226 | startService(networkIntent);
227 |
228 | // Everything is done in the NetworkService if no dialog is opened
229 | if (!userPrefs.isOpenDialog()){
230 | return;
231 | }
232 |
233 | if (userPrefs.isAutoDescription()) {
234 | stopEarlyAutoLoad(
235 | defaults.getDescription(),
236 | R.id.loading_description,
237 | R.id.description,
238 | R.string.loading_description_hint,
239 | LOADER_DESCRIPTION);
240 | }
241 |
242 | if (userPrefs.isAutoTitle()) {
243 | stopEarlyAutoLoad(
244 | defaults.getTitle(),
245 | R.id.loading_title,
246 | R.id.title,
247 | R.string.loading_title_hint,
248 | LOADER_TITLE);
249 | }
250 | }
251 |
252 | /**
253 | * helper function for stopEarlyAutoload closure.
254 | * It is not very useful otherwise
255 | *
256 | * @param loaderId
257 | * @param value
258 | */
259 | private void setLoading(int loaderId, boolean value) {
260 | switch (loaderId) {
261 | case LOADER_TITLE:
262 | isLoadingTitle = value;
263 | break;
264 | case LOADER_DESCRIPTION:
265 | isLoadingDescription = value;
266 | break;
267 | case LOADER_PREFETCH:
268 | isPrefetching = value;
269 | break;
270 | default:
271 | break;
272 | }
273 | }
274 |
275 | private void updateLoadersVisibility() {
276 | View titleLoader = findViewById(R.id.loading_title);
277 | if (isLoadingTitle || isPrefetching) {
278 | titleLoader.setVisibility(View.VISIBLE);
279 | } else {
280 | titleLoader.setVisibility(View.GONE);
281 | }
282 |
283 | View descriptionLoader = findViewById(R.id.loading_description);
284 | if (isLoadingDescription || isPrefetching) {
285 | descriptionLoader.setVisibility(View.VISIBLE);
286 | } else {
287 | descriptionLoader.setVisibility(View.GONE);
288 | }
289 | }
290 |
291 | private void stopEarlyAutoLoad(String defaultValue, final int loader, final int field, int hint, final int loaderId) {
292 | if ("".equals(defaultValue)) {
293 | ((EditText) findViewById(field)).setHint(hint);
294 |
295 | // If in the meanwhile the user type text in the field, stop retrieving the data
296 | ((EditText) findViewById(field)).addTextChangedListener(new TextWatcher() {
297 | @Override
298 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {
299 | setLoading(loaderId, false);
300 | findViewById(loader).setVisibility(View.GONE);
301 | ((EditText) findViewById(field)).removeTextChangedListener(this);
302 | }
303 |
304 | @Override
305 | public void onTextChanged(CharSequence s, int start, int before, int count) {
306 | // Nothing to be done
307 | }
308 |
309 | @Override
310 | public void afterTextChanged(Editable s) {
311 | // Nothing to be done
312 | }
313 | });
314 | } else {
315 | setLoading(loaderId, false);
316 | updateTitle(defaultValue, false);
317 | }
318 | }
319 |
320 | private void updateTitle(String title, boolean isError) {
321 | EditText titleEdit = findViewById(R.id.title);
322 |
323 | titleEdit.setHint(R.string.title_hint);
324 |
325 | if (isError) {
326 | titleEdit.setHint(R.string.error_retrieving_title);
327 | } else {
328 | titleEdit.setText(title);
329 | }
330 |
331 | updateLoadersVisibility();
332 | }
333 |
334 | private void updateDescription(String description, boolean isError) {
335 | EditText descriptionEdit = findViewById(R.id.description);
336 | descriptionEdit.setHint(R.string.description_hint);
337 |
338 |
339 | if (isError) {
340 | descriptionEdit.setHint(R.string.error_retrieving_description);
341 | } else {
342 | descriptionEdit.setText(description);
343 | }
344 | updateLoadersVisibility();
345 | }
346 |
347 | private void updateTags(String tags, boolean isError) {
348 | EditText tagsEdit = findViewById(R.id.tags);
349 |
350 | if (isError) {
351 | // Display nothing
352 | Log.e(LOGGER_NAME, "error retrieving tags");
353 | } else {
354 | tagsEdit.setText(tags);
355 | }
356 | }
357 |
358 | private void updatePrivate(boolean isPrivate) {
359 | Checkable tagsEdit = findViewById(R.id.private_share);
360 | tagsEdit.setChecked(isPrivate);
361 | }
362 |
363 | private void updateTweet(boolean tweet) {
364 | Checkable tweetCheck = findViewById(R.id.tweet);
365 | tweetCheck.setChecked(tweet);
366 | }
367 |
368 | private void updateToot(boolean toot) {
369 | Checkable tootCheck = findViewById(R.id.toot);
370 | tootCheck.setChecked(toot);
371 | }
372 |
373 | private Link linkFromUserInput() {
374 | return new Link(
375 | ((EditText) findViewById(R.id.url)).getText().toString(),
376 | ((EditText) findViewById(R.id.title)).getText().toString(),
377 | ((EditText) findViewById(R.id.description)).getText().toString(),
378 | ((EditText) findViewById(R.id.tags)).getText().toString(),
379 | ((Checkable) findViewById(R.id.private_share)).isChecked(),
380 | (ShaarliAccount) ((Spinner) findViewById(R.id.chooseAccount)).getSelectedItem(),
381 | ((Checkable) findViewById(R.id.tweet)).isChecked(),
382 | ((Checkable) findViewById(R.id.toot)).isChecked(),
383 | null,
384 | null
385 | );
386 | }
387 |
388 | class AccountSpinnerListener implements AdapterView.OnItemSelectedListener {
389 | @Override
390 | public void onItemSelected(AdapterView> parent, View view, int position, long id) {
391 | setLoading(LOADER_DESCRIPTION, true);
392 | setLoading(LOADER_TITLE, true);
393 | updateLoadersVisibility();
394 | Link defaults = linkFromUserInput();
395 | // Override text values to avoid messing with `seemsNewLink`
396 | defaults.setTitle("");
397 | defaults.setDescription("");
398 | defaults.setTags("");
399 |
400 | prefetchLink(defaults);
401 | }
402 |
403 | @Override
404 | public void onNothingSelected(AdapterView> parent) {
405 | // pass
406 | }
407 | }
408 |
409 | @Override
410 | public boolean onOptionsItemSelected(MenuItem item) {
411 | int id = item.getItemId();
412 | if (id == R.id.action_share) {
413 | saveAndShare();
414 | } else {
415 | return true;
416 | }
417 | return super.onOptionsItemSelected(item);
418 |
419 | }
420 |
421 | /**
422 | * Start the network service to send the link
423 | * @param link link to send
424 | */
425 | private void sendLink(@NonNull Link link) {
426 | Intent networkIntent = new Intent(this, NetworkService.class);
427 | networkIntent.putExtra("action", NetworkService.INTENT_POST);
428 | networkIntent.putExtra("link", link);
429 | networkIntent.putExtra(
430 | NetworkService.EXTRA_MESSENGER,
431 | new Messenger(new NetworkHandler())
432 | );
433 |
434 | startService(networkIntent);
435 | }
436 |
437 | private class NetworkHandler extends Handler {
438 | /**
439 | * Handle the arrival of a message coming from the network service.
440 | *
441 | * @param msg the message given by the service
442 | */
443 | @Override
444 | public void handleMessage(Message msg) {
445 | switch (msg.arg1) {
446 | case NetworkService.RETRIEVE_TITLE_ID:
447 | handleRetrieveTitleId(msg);
448 | break;
449 | case NetworkService.PREFETCH_LINK:
450 | handlePrefetchLink(msg);
451 | break;
452 | default:
453 | Toast.makeText(getApplicationContext(), R.string.error_unknown, Toast.LENGTH_LONG).show();
454 | Log.e("NETWORK_MSG", "Unknown network intent received: " + msg.arg1);
455 | break;
456 | }
457 | }
458 |
459 | private void handleRetrieveTitleId(final Message msg) {
460 | Log.i("NETWORK_MSG", "Title or description retrieved");
461 | if (userPrefs.isOpenDialog()) {
462 | final String title = ((String[]) msg.obj)[0];
463 | final String description = ((String[]) msg.obj)[1];
464 |
465 | if (isLoadingTitle && userPrefs.isAutoTitle()) {
466 | updateTitle(title, "".equals(title));
467 | }
468 | if (isLoadingDescription && userPrefs.isAutoDescription()) {
469 | updateDescription(description, "".equals(description));
470 | }
471 | setLoading(LOADER_TITLE, false);
472 | setLoading(LOADER_DESCRIPTION, false);
473 | updateLoadersVisibility();
474 | }
475 | }
476 |
477 | private void handlePrefetchLink(final Message msg) {
478 | Log.i("NETWORK_MSG", "Link prefetched");
479 |
480 | MenuItem isEditedIcon = menu.findItem(R.id.editing);
481 | setLoading(LOADER_PREFETCH, false);
482 | if (userPrefs.isOpenDialog()) {
483 | Link prefetchedLink = (Link) msg.obj;
484 |
485 | isNotNewLink = prefetchedLink.seemsNotNew();
486 | Log.i("PREFETCH_LINK", "SeemsNotNew? " + isNotNewLink);
487 | // Prefetching has priority over previously saved data since it
488 | // coming from Shaarli directly
489 | if (isNotNewLink) {
490 | defaults = prefetchedLink;
491 |
492 | // Update the interface
493 | if (defaults.getTitle().length() > 0) {
494 | updateTitle(defaults.getTitle(), false);
495 | }
496 | if (defaults.getDescription().length() > 0) {
497 | updateDescription(defaults.getDescription(), false);
498 | }
499 | if (defaults.getTagList().size() > 0) {
500 | updateTags(defaults.getTags(), false);
501 | }
502 | updatePrivate(defaults.isPrivate());
503 | updateTweet(defaults.isTweet());
504 | updateToot(defaults.isToot());
505 |
506 | // Show that we are editing an existing entry
507 | isEditedIcon.setVisible(true);
508 | } else {
509 | isEditedIcon.setVisible(false);
510 | }
511 | // prefetch success: stop other loaders
512 | setLoading(LOADER_TITLE, false);
513 | setLoading(LOADER_DESCRIPTION, false);
514 | updateLoadersVisibility();
515 | }
516 | }
517 | }
518 | }
519 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/dao/AccountsSource.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.dao;
2 |
3 | import android.content.ContentValues;
4 | import android.content.Context;
5 | import android.content.SharedPreferences;
6 | import android.database.Cursor;
7 | import android.database.SQLException;
8 | import android.database.sqlite.SQLiteDatabase;
9 | import android.text.SpannableString;
10 | import android.text.Spanned;
11 | import android.text.TextUtils;
12 | import android.widget.MultiAutoCompleteTextView;
13 |
14 | import androidx.annotation.NonNull;
15 | import androidx.annotation.Nullable;
16 |
17 | import com.dimtion.shaarlier.R;
18 | import com.dimtion.shaarlier.helper.EncryptionHelper;
19 | import com.dimtion.shaarlier.models.ShaarliAccount;
20 |
21 | import java.util.ArrayList;
22 | import java.util.List;
23 |
24 | import javax.crypto.SecretKey;
25 |
26 | /**
27 | * Created by dimtion on 11/05/2015.
28 | * API for managing accounts
29 | */
30 | public class AccountsSource {
31 |
32 | private final String[] allColumns = {
33 | MySQLiteHelper.ACCOUNTS_COLUMN_ID,
34 | MySQLiteHelper.ACCOUNTS_COLUMN_URL_SHAARLI,
35 | MySQLiteHelper.ACCOUNTS_COLUMN_USERNAME,
36 | MySQLiteHelper.ACCOUNTS_COLUMN_PASSWORD_CYPHER,
37 | MySQLiteHelper.ACCOUNTS_COLUMN_SHORT_NAME,
38 | MySQLiteHelper.ACCOUNTS_COLUMN_IV,
39 | MySQLiteHelper.ACCOUNTS_COLUMN_VALIDATE_CERT,
40 | MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_USERNAME,
41 | MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_PASSWORD_CYPHER,
42 | MySQLiteHelper.ACCOUNTS_COLUMN_REST_API_KEY,
43 | };
44 | private final MySQLiteHelper dbHelper;
45 | private final Context mContext;
46 | private SQLiteDatabase db;
47 |
48 | public AccountsSource(Context context) {
49 | dbHelper = new MySQLiteHelper(context);
50 | this.mContext = context;
51 | }
52 |
53 | public void rOpen() throws SQLException {
54 | db = dbHelper.getReadableDatabase();
55 | }
56 |
57 | public void wOpen() throws SQLException {
58 | db = dbHelper.getWritableDatabase();
59 | }
60 |
61 | public void close() {
62 | dbHelper.close();
63 | }
64 |
65 | public ShaarliAccount createAccount(String urlShaarli, String username, String password, String basicAuthUsername, String basicAuthPassword, String shortName, boolean validateCert, String restAPIKey) throws Exception {
66 | ContentValues values = new ContentValues();
67 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_URL_SHAARLI, urlShaarli);
68 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_USERNAME, username);
69 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_USERNAME, basicAuthUsername);
70 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_REST_API_KEY, restAPIKey);
71 |
72 | // Generate the iv :
73 | byte[] iv = EncryptionHelper.generateInitialVector();
74 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_IV, iv);
75 |
76 | byte[] password_cipher = encryptPassword(password, iv);
77 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_PASSWORD_CYPHER, password_cipher);
78 |
79 | byte[] basic_password_cipher = encryptPassword(basicAuthPassword, iv);
80 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_PASSWORD_CYPHER, basic_password_cipher);
81 |
82 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_SHORT_NAME, shortName);
83 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_VALIDATE_CERT, validateCert ? 1:0 ); // Convert bool to int
84 |
85 | long insertId = db.insert(MySQLiteHelper.TABLE_ACCOUNTS, null, values);
86 | return getShaarliAccountById(insertId);
87 | }
88 |
89 | public List getAllAccounts() {
90 | List accounts = new ArrayList<>();
91 |
92 | Cursor cursor = db.query(MySQLiteHelper.TABLE_ACCOUNTS, allColumns, null, null, null, null, null);
93 | cursor.moveToFirst();
94 | while (!cursor.isAfterLast()) {
95 | ShaarliAccount account = cursorToAccount(cursor);
96 | if (account != null)
97 | accounts.add(account);
98 | cursor.moveToNext();
99 | }
100 |
101 | cursor.close();
102 | return accounts;
103 | }
104 |
105 | private SecretKey getSecretKey() {
106 | String id = mContext.getString(R.string.params);
107 | SharedPreferences prefs = this.mContext.getSharedPreferences(id, Context.MODE_PRIVATE);
108 |
109 | String sKey = prefs.getString(this.mContext.getString(R.string.dbKey), "");
110 | return EncryptionHelper.stringToSecretKey(sKey);
111 | }
112 |
113 | private byte[] encryptPassword(String clearPassword, byte[] initialVector) throws Exception {
114 | SecretKey key = getSecretKey();
115 | byte[] encoded = EncryptionHelper.stringToBase64(clearPassword);
116 | return EncryptionHelper.encrypt(encoded, key, initialVector);
117 | }
118 |
119 | private String decryptPassword(byte[] cipherData, byte[] initialVector) throws Exception {
120 | SecretKey key = getSecretKey();
121 | byte[] encodedPassword = EncryptionHelper.decrypt(cipherData, key, initialVector);
122 | return EncryptionHelper.base64ToString(encodedPassword);
123 | }
124 |
125 | @Nullable
126 | public ShaarliAccount getShaarliAccountById(long id) {
127 | rOpen();
128 | Cursor cursor = db.query(MySQLiteHelper.TABLE_ACCOUNTS, allColumns, MySQLiteHelper.ACCOUNTS_COLUMN_ID + " = " + id, null,
129 | null, null, null);
130 | cursor.moveToFirst();
131 |
132 | ShaarliAccount account = cursorToAccount(cursor);
133 | cursor.close();
134 | close();
135 |
136 | return account;
137 | }
138 |
139 | @Nullable
140 | private ShaarliAccount cursorToAccount(@NonNull Cursor cursor) {
141 | if (cursor.isAfterLast())
142 | return null;
143 |
144 | ShaarliAccount account = new ShaarliAccount();
145 | account.setId(cursor.getLong(0));
146 | account.setUrlShaarli(cursor.getString(1));
147 | account.setUsername(cursor.getString(2));
148 | account.setInitialVector(cursor.getBlob(5));
149 | account.setValidateCert(cursor.getInt(6) == 1); // Convert int to bool
150 | account.setBasicAuthUsername(cursor.getString(7));
151 | account.setRestAPIKey(cursor.getString(9));
152 |
153 | byte[] password_cypher = cursor.getBlob(3);
154 | String password;
155 | try {
156 | password = decryptPassword(password_cypher, account.getInitialVector());
157 | } catch (Exception e) {
158 | e.printStackTrace();
159 | return null;
160 | }
161 | account.setPassword(password);
162 |
163 | byte[] basic_password_cypher = cursor.getBlob(8);
164 | String basic_password;
165 | try {
166 | basic_password = decryptPassword(basic_password_cypher, account.getInitialVector());
167 | } catch (Exception e) {
168 | e.printStackTrace();
169 | basic_password = "";
170 | }
171 | account.setBasicAuthPassword(basic_password);
172 |
173 | account.setShortName(cursor.getString(4));
174 |
175 | return account;
176 | }
177 |
178 | public void deleteAccount(ShaarliAccount account) {
179 | db.delete(MySQLiteHelper.TABLE_ACCOUNTS, MySQLiteHelper.ACCOUNTS_COLUMN_ID + " = " + account.getId(), null);
180 | }
181 |
182 | public void editAccount(ShaarliAccount account) throws Exception {
183 | String QUERY_WHERE = MySQLiteHelper.ACCOUNTS_COLUMN_ID + " = " + account.getId();
184 | ContentValues values = new ContentValues();
185 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_URL_SHAARLI, account.getUrlShaarli());
186 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_USERNAME, account.getUsername());
187 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_REST_API_KEY, account.getRestAPIKey());
188 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_USERNAME, account.getBasicAuthUsername());
189 |
190 | // Generate a new iv :
191 | account.setInitialVector(EncryptionHelper.generateInitialVector());
192 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_IV, account.getInitialVector());
193 |
194 | byte[] password_cipher = encryptPassword(account.getPassword(), account.getInitialVector());
195 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_PASSWORD_CYPHER, password_cipher);
196 |
197 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_SHORT_NAME, account.getShortName());
198 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_VALIDATE_CERT, account.isValidateCert() ? 1:0); // convert bool to int
199 | byte[] basic_password_cipher = encryptPassword(account.getBasicAuthPassword(), account.getInitialVector());
200 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_PASSWORD_CYPHER, basic_password_cipher);
201 |
202 | db.update(MySQLiteHelper.TABLE_ACCOUNTS, values, QUERY_WHERE, null);
203 | }
204 |
205 | public ShaarliAccount getDefaultAccount() {
206 | SharedPreferences prefs = this.mContext.getSharedPreferences(this.mContext.getString(R.string.params), Context.MODE_PRIVATE);
207 | long defaultAccountId = prefs.getLong(this.mContext.getString(R.string.p_default_account), -1);
208 |
209 | ShaarliAccount defaultAccount = getShaarliAccountById(defaultAccountId);
210 | if (defaultAccount == null) {
211 | rOpen();
212 | Cursor cursor = db.query(MySQLiteHelper.TABLE_ACCOUNTS, allColumns, null, null, null, null, MySQLiteHelper.ACCOUNTS_COLUMN_ID, "1");
213 | cursor.moveToFirst();
214 | defaultAccount = cursorToAccount(cursor);
215 | cursor.close();
216 | close();
217 | }
218 | return defaultAccount;
219 | }
220 |
221 | /**
222 | * Created by dimtion on 22/02/2015.
223 | * Custom tokenizer, found on : http://stackoverflow.com/a/4596652/1582589 by vsm *
224 | */
225 |
226 |
227 | public static class SpaceTokenizer implements MultiAutoCompleteTextView.Tokenizer {
228 |
229 | public int findTokenStart(CharSequence text, int cursor) {
230 | int i = cursor;
231 |
232 | while (i > 0 && text.charAt(i - 1) != ' ') {
233 | i--;
234 | }
235 | while (i < cursor && text.charAt(i) == ' ') {
236 | i++;
237 | }
238 |
239 | return i;
240 | }
241 |
242 | public int findTokenEnd(CharSequence text, int cursor) {
243 | int i = cursor;
244 | int len = text.length();
245 |
246 | while (i < len) {
247 | if (text.charAt(i) == ' ') {
248 | return i;
249 | } else {
250 | i++;
251 | }
252 | }
253 |
254 | return len;
255 | }
256 |
257 | public CharSequence terminateToken(CharSequence text) {
258 | int i = text.length();
259 |
260 | while (i > 0 && text.charAt(i - 1) == ' ') {
261 | i--;
262 | }
263 |
264 | if (i > 0 && text.charAt(i - 1) == ' ') {
265 | return text;
266 | } else {
267 | if (text instanceof Spanned) {
268 | SpannableString sp = new SpannableString(text + " ");
269 | TextUtils.copySpansFrom((Spanned) text, 0, text.length(),
270 | Object.class, sp, 0);
271 | return sp;
272 | } else {
273 | return text + " ";
274 | }
275 | }
276 | }
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/dao/MySQLiteHelper.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.dao;
2 |
3 | import android.content.ContentValues;
4 | import android.content.Context;
5 | import android.content.SharedPreferences;
6 | import android.database.sqlite.SQLiteDatabase;
7 | import android.database.sqlite.SQLiteOpenHelper;
8 | import android.util.Log;
9 |
10 | import com.dimtion.shaarlier.R;
11 | import com.dimtion.shaarlier.helper.EncryptionHelper;
12 |
13 | import java.security.NoSuchAlgorithmException;
14 |
15 | import javax.crypto.SecretKey;
16 |
17 | /**
18 | * Created by dimtion on 11/05/2015.
19 | * This class update the db scheme when necessary
20 | */
21 | class MySQLiteHelper extends SQLiteOpenHelper {
22 | // Table: accounts
23 | static final String TABLE_ACCOUNTS = "accounts";
24 | static final String ACCOUNTS_COLUMN_ID = "_id";
25 | static final String ACCOUNTS_COLUMN_URL_SHAARLI = "url_shaarli";
26 | static final String ACCOUNTS_COLUMN_USERNAME = "username";
27 | static final String ACCOUNTS_COLUMN_PASSWORD_CYPHER = "password_cypher";
28 | static final String ACCOUNTS_COLUMN_SHORT_NAME = "short_name";
29 | static final String ACCOUNTS_COLUMN_IV = "initial_vector";
30 | static final String ACCOUNTS_COLUMN_VALIDATE_CERT = "validate_cert";
31 | static final String ACCOUNTS_COLUMN_BASIC_AUTH_USERNAME = "basic_auth_username";
32 | static final String ACCOUNTS_COLUMN_BASIC_AUTH_PASSWORD_CYPHER = "basic_auth_password_cypher";
33 | static final String ACCOUNTS_COLUMN_REST_API_KEY = "rest_api_key";
34 | // Table: tags
35 | static final String TABLE_TAGS = "tags";
36 | private static final int DATABASE_VERSION = 4;
37 | private static final String CREATE_TABLE_ACCOUNTS = "create table "
38 | + TABLE_ACCOUNTS + " ("
39 | + ACCOUNTS_COLUMN_ID + " integer primary key autoincrement, "
40 | + ACCOUNTS_COLUMN_URL_SHAARLI + " text NOT NULL, "
41 | + ACCOUNTS_COLUMN_USERNAME + " text NOT NULL, "
42 | + ACCOUNTS_COLUMN_PASSWORD_CYPHER + " BLOB, "
43 | + ACCOUNTS_COLUMN_SHORT_NAME + " text DEFAULT '', "
44 | + ACCOUNTS_COLUMN_IV + " BLOB,"
45 | + ACCOUNTS_COLUMN_VALIDATE_CERT + " integer DEFAULT 1,"
46 | + ACCOUNTS_COLUMN_BASIC_AUTH_USERNAME + " text NOT NULL, "
47 | + ACCOUNTS_COLUMN_BASIC_AUTH_PASSWORD_CYPHER + " BLOB, "
48 | + ACCOUNTS_COLUMN_REST_API_KEY + " text NOT NULL);";
49 | static final String TAGS_COLUMN_ID = "_id";
50 | static final String TAGS_COLUMN_ID_ACCOUNT = "account_id";
51 | static final String TAGS_COLUMN_TAG = "tag";
52 | private static final String DATABASE_NAME = "shaarlier.db";
53 | private static final String CREATE_TABLE_TAGS = "create table "
54 | + TABLE_TAGS + " ("
55 | + TAGS_COLUMN_ID + " integer primary key autoincrement, "
56 | + TAGS_COLUMN_ID_ACCOUNT + " integer NOT NULL, "
57 | + TAGS_COLUMN_TAG + " text NOT NULL ) ;";
58 | // Database updates
59 | private static final String[][] UPDATE_DB = {
60 | // UPDATE 1 -> 2
61 | {
62 | "ALTER TABLE " + TABLE_ACCOUNTS +
63 | " ADD `" + ACCOUNTS_COLUMN_VALIDATE_CERT + "` integer NOT NULL DEFAULT 1;",
64 | },
65 | // UPDATE 2 -> 3
66 | {
67 | "ALTER TABLE " + TABLE_ACCOUNTS +
68 | " ADD `" + ACCOUNTS_COLUMN_BASIC_AUTH_USERNAME + "` text NOT NULL DEFAULT ``; ",
69 | "ALTER TABLE " + TABLE_ACCOUNTS +
70 | " ADD `" + ACCOUNTS_COLUMN_BASIC_AUTH_PASSWORD_CYPHER + "` BLOB;"
71 | },
72 | // UPDATE 3 -> 4
73 | {
74 | "ALTER TABLE " + TABLE_ACCOUNTS +
75 | " ADD `" + ACCOUNTS_COLUMN_REST_API_KEY + "` text NOT NULL DEFAULT ``; "
76 | }
77 | };
78 |
79 | private final Context mContext;
80 |
81 | MySQLiteHelper(Context context) {
82 | super(context, DATABASE_NAME, null, DATABASE_VERSION);
83 | this.mContext = context;
84 | }
85 |
86 | @Override
87 | public void onCreate(SQLiteDatabase db) {
88 | db.execSQL(CREATE_TABLE_ACCOUNTS);
89 | db.execSQL(CREATE_TABLE_TAGS);
90 |
91 | // Create a secret key
92 | String id = mContext.getString(R.string.params);
93 | SharedPreferences prefs = this.mContext.getSharedPreferences(id, Context.MODE_PRIVATE);
94 | SharedPreferences.Editor editor = prefs.edit();
95 | SecretKey key;
96 | try {
97 | key = EncryptionHelper.generateKey();
98 | String sKey = EncryptionHelper.secretKeyToString(key);
99 |
100 | editor.putString(mContext.getString(R.string.dbKey), sKey);
101 | editor.apply();
102 | } catch (NoSuchAlgorithmException e) {
103 | e.printStackTrace();
104 | key = null;
105 | Log.e("MySQLiteHelper", e.getMessage());
106 | }
107 | updateFromV0(db, prefs, key);
108 | }
109 |
110 | /**
111 | * Update from no database to DB
112 | */
113 | @Deprecated
114 | private void updateFromV0(SQLiteDatabase db, SharedPreferences prefs, SecretKey key) {
115 | String url = prefs.getString(mContext.getString(R.string.p_user_url), "");
116 | String usr = prefs.getString(mContext.getString(R.string.p_username), "");
117 | String pwd = prefs.getString(mContext.getString(R.string.p_password), "");
118 | String busr = prefs.getString(mContext.getString(R.string.p_basic_username), "");
119 | String bpwd = prefs.getString(mContext.getString(R.string.p_basic_password), "");
120 | int protocol = prefs.getInt(mContext.getString(R.string.p_protocol), 0);
121 | boolean isValidated = prefs.getBoolean(mContext.getString(R.string.p_validated), false);
122 |
123 | String[] prot = {"http://", "https://"};
124 | try {
125 | if (isValidated) {
126 | ContentValues values = new ContentValues();
127 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_URL_SHAARLI, prot[protocol] + url);
128 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_USERNAME, usr);
129 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_USERNAME, busr);
130 |
131 | // Generate the iv :
132 | byte[] iv = EncryptionHelper.generateInitialVector();
133 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_IV, iv);
134 |
135 | byte[] encoded = EncryptionHelper.stringToBase64(pwd);
136 | byte[] password_cipher = EncryptionHelper.encrypt(encoded, key, iv);
137 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_PASSWORD_CYPHER, password_cipher);
138 |
139 | byte[] basic_encoded = EncryptionHelper.stringToBase64(bpwd);
140 | byte[] basic_password_cipher = EncryptionHelper.encrypt(basic_encoded, key, iv);
141 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_PASSWORD_CYPHER, basic_password_cipher);
142 |
143 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_SHORT_NAME, "Shaarli");
144 |
145 | db.insert(MySQLiteHelper.TABLE_ACCOUNTS, null, values);
146 | }
147 | } catch (Exception e) {
148 | Log.e("MySQLiteHelper", e.getMessage());
149 | }
150 | }
151 |
152 | @Override
153 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
154 | Log.w(MySQLiteHelper.class.getName(),
155 | "Upgrading database from version " + oldVersion + " to "
156 | + newVersion + ", which will destroy all old data");
157 | for (int i = oldVersion - 1; i < newVersion - 1; i++) {
158 | for (String query :
159 | UPDATE_DB[i]) {
160 | db.execSQL(query);
161 | }
162 | }
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/dao/TagsSource.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.dao;
2 |
3 | import android.content.ContentValues;
4 | import android.content.Context;
5 | import android.database.Cursor;
6 | import android.database.SQLException;
7 | import android.database.sqlite.SQLiteDatabase;
8 |
9 | import androidx.annotation.NonNull;
10 |
11 | import com.dimtion.shaarlier.models.ShaarliAccount;
12 | import com.dimtion.shaarlier.models.Tag;
13 |
14 | import java.util.ArrayList;
15 | import java.util.List;
16 |
17 | /**
18 | * Created by dimtion on 12/05/2015.
19 | * Interface between the table TAGS and the JAVA objects
20 | */
21 | public class TagsSource {
22 | private final String[] allColumns = {MySQLiteHelper.TAGS_COLUMN_ID,
23 | MySQLiteHelper.TAGS_COLUMN_ID_ACCOUNT,
24 | MySQLiteHelper.TAGS_COLUMN_TAG};
25 | private final MySQLiteHelper dbHelper;
26 | private SQLiteDatabase db;
27 |
28 | public TagsSource(final Context context) {
29 | dbHelper = new MySQLiteHelper(context);
30 | }
31 |
32 | public void rOpen() throws SQLException {
33 | db = dbHelper.getReadableDatabase();
34 | }
35 |
36 | public void wOpen() throws SQLException {
37 | db = dbHelper.getWritableDatabase();
38 | }
39 |
40 | public void close() {
41 | dbHelper.close();
42 | }
43 |
44 | public List getAllTags() {
45 | List tags = new ArrayList<>();
46 |
47 | Cursor cursor = db.query(MySQLiteHelper.TABLE_TAGS, allColumns, null, null, null, null, null);
48 | cursor.moveToFirst();
49 | while (!cursor.isAfterLast()) {
50 | Tag account = cursorToTag(cursor);
51 | tags.add(account);
52 | cursor.moveToNext();
53 | }
54 |
55 | cursor.close();
56 | return tags;
57 | }
58 |
59 | public Tag createTag(final ShaarliAccount masterAccount, final String value) {
60 | Tag tag = new Tag();
61 | tag.setMasterAccount(masterAccount);
62 | tag.setValue(value.trim());
63 |
64 | ContentValues values = new ContentValues();
65 | values.put(MySQLiteHelper.TAGS_COLUMN_ID_ACCOUNT, masterAccount.getId());
66 | values.put(MySQLiteHelper.TAGS_COLUMN_TAG, tag.getValue());
67 |
68 | // If existing, do nothing :
69 | String[] getTagArgs = {String.valueOf(tag.getMasterAccountId()), tag.getValue()};
70 |
71 | Cursor cursor = db.query(MySQLiteHelper.TABLE_TAGS, allColumns,
72 | MySQLiteHelper.TAGS_COLUMN_ID_ACCOUNT + " = ? AND " +
73 | MySQLiteHelper.TAGS_COLUMN_TAG + " = ?",
74 | getTagArgs, null, null, null);
75 | try {
76 | cursor.moveToFirst();
77 | if (cursor.isAfterLast()) {
78 | long insertId = db.insert(MySQLiteHelper.TABLE_TAGS, null, values);
79 | tag.setId(insertId);
80 | return tag;
81 | } else {
82 | tag = cursorToTag(cursor);
83 | }
84 | } catch (Exception e){
85 | tag = null;
86 | } finally {
87 | cursor.close();
88 | }
89 | return tag;
90 | }
91 |
92 | private Tag cursorToTag(@NonNull Cursor cursor) { // If necessary (later), load the full account in the tag
93 | Tag tag = new Tag();
94 | tag.setId(cursor.getLong(0));
95 | tag.setMasterAccountId(cursor.getLong(1));
96 | tag.setValue(cursor.getString(2));
97 | return tag;
98 | }
99 |
100 | private void deleteAllTags() {
101 | db.delete(MySQLiteHelper.TABLE_TAGS, null, null);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/exceptions/UnsupportedIntent.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.exceptions;
2 |
3 | public class UnsupportedIntent extends Exception {
4 | public UnsupportedIntent() {
5 | super();
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/helper/AutoCompleteWrapper.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.helper;
2 |
3 | import android.app.Activity;
4 | import android.app.AlertDialog;
5 | import android.content.Context;
6 | import android.content.DialogInterface;
7 | import android.os.AsyncTask;
8 | import android.util.Log;
9 | import android.widget.MultiAutoCompleteTextView;
10 | import android.widget.Toast;
11 |
12 | import com.dimtion.shaarlier.R;
13 | import com.dimtion.shaarlier.dao.AccountsSource;
14 | import com.dimtion.shaarlier.dao.TagsSource;
15 | import com.dimtion.shaarlier.network.NetworkManager;
16 | import com.dimtion.shaarlier.network.NetworkUtils;
17 | import com.dimtion.shaarlier.utils.FuzzyArrayAdapter;
18 | import com.dimtion.shaarlier.models.ShaarliAccount;
19 | import com.dimtion.shaarlier.models.Tag;
20 |
21 | import java.util.List;
22 |
23 | /**
24 | * Created by dimtion on 21/02/2015.
25 | * Inspired from : http://stackoverflow.com/a/5051180
26 | * and : http://www.claytical.com/blog/android-dynamic-autocompletion-using-google-places-api
27 | */
28 | public class AutoCompleteWrapper {
29 |
30 | private final MultiAutoCompleteTextView a_textView;
31 | private final Context a_context;
32 | private final FuzzyArrayAdapter adapter;
33 |
34 | public AutoCompleteWrapper(final MultiAutoCompleteTextView textView, Context context) {
35 | this.a_textView = textView;
36 | this.a_context = context;
37 |
38 | this.a_textView.setTokenizer(new AccountsSource.SpaceTokenizer());
39 |
40 | this.adapter = new FuzzyArrayAdapter<>(a_context, R.layout.tags_list);
41 | this.a_textView.setAdapter(this.adapter);
42 | this.a_textView.setThreshold(1);
43 | updateTagsView();
44 |
45 | AutoCompleteRetriever task = new AutoCompleteRetriever();
46 | task.execute();
47 | }
48 |
49 | private void updateTagsView() {
50 | try {
51 | TagsSource tagsSource = new TagsSource(a_context);
52 | tagsSource.rOpen();
53 | List tagList = tagsSource.getAllTags();
54 | tagsSource.close();
55 |
56 | this.adapter.clear();
57 | this.adapter.addAll(tagList);
58 | this.adapter.notifyDataSetChanged();
59 |
60 | this.a_textView.setAdapter(this.adapter);
61 |
62 | } catch (Exception e) {
63 | sendReport(e);
64 | }
65 | }
66 |
67 | private class AutoCompleteRetriever extends AsyncTask {
68 | private Exception mError;
69 | @Override
70 | protected Boolean doInBackground(String... foo) {
71 | AccountsSource accountsSource = new AccountsSource(a_context);
72 | accountsSource.rOpen();
73 | List accounts = accountsSource.getAllAccounts();
74 | TagsSource tagsSource = new TagsSource(a_context);
75 | tagsSource.wOpen();
76 |
77 | boolean success = true;
78 | /* For the moment we keep all the tags, if later somebody wants to have the tags
79 | ** separated for each accounts, we will see
80 | */
81 | for (ShaarliAccount account : accounts) {
82 | // Download tags:
83 | NetworkManager manager = NetworkUtils.getNetworkManager(account);
84 | try {
85 | if (!manager.isCompatibleShaarli() || !manager.login()) {
86 | throw new Exception(account + ": Login Error");
87 | }
88 | List tags = manager.retrieveTags();
89 | Log.d("TAGS", tags.toString());
90 | for (String tag : tags) {
91 | tagsSource.createTag(account, tag.trim());
92 | }
93 | } catch (Exception e) {
94 | mError = e;
95 | success = false;
96 | Log.e("ERROR", e.toString());
97 | }
98 | }
99 |
100 | tagsSource.close();
101 | accountsSource.close();
102 | return success;
103 | }
104 |
105 | // onPostExecute displays the results of the AsyncTask.
106 | @Override
107 | protected void onPostExecute(Boolean r) {
108 | if(!r) {
109 | String error = (mError != null) ? mError.getMessage() : "";
110 | Toast.makeText(a_context, a_context.getString(R.string.error_retrieving_tags) + " - " + error, Toast.LENGTH_LONG).show();
111 | } else {
112 | updateTagsView();
113 | }
114 | }
115 | }
116 |
117 | private void sendReport(final Exception error) {
118 | final Activity activity = (Activity) a_context;
119 | AlertDialog.Builder builder = new AlertDialog.Builder(a_context);
120 |
121 | builder.setMessage("Would you like to report this issue ?").setTitle("REPORT - Shaarlier: add link");
122 |
123 |
124 | final String extra = ""; // "Url Shaarli: " + account.getUrlShaarli();
125 |
126 | builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
127 | public void onClick(DialogInterface dialog, int id) {
128 | DebugHelper.sendMailDev(activity, "REPORT - Shaarlier: load tags", DebugHelper.generateReport(error, activity, extra));
129 | }
130 | });
131 | builder.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
132 | public void onClick(DialogInterface dialog, int id) {
133 | // User cancelled the dialog
134 | }
135 | });
136 | AlertDialog dialog = builder.create();
137 | dialog.show();
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/helper/DebugHelper.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.helper;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.content.pm.PackageManager;
6 | import android.net.Uri;
7 | import android.os.Build;
8 | import android.util.Log;
9 |
10 | import com.dimtion.shaarlier.R;
11 |
12 | import java.text.DateFormat;
13 | import java.text.SimpleDateFormat;
14 | import java.util.Date;
15 | import java.util.TimeZone;
16 |
17 |
18 | /**
19 | * Created by dimtion on 16/05/2015.
20 | * A class to help debugging and error reporting
21 | */
22 | public class DebugHelper {
23 |
24 | public static void sendMailDev(Activity context, String subject, String content) {
25 | Log.d("sendMailDev", content);
26 | Intent intent = new Intent(Intent.ACTION_SENDTO);
27 | intent.setData(Uri.parse("mailto:"));
28 | intent.putExtra(Intent.EXTRA_EMAIL, new String[]{context.getString(R.string.developer_mail)});
29 | intent.putExtra(Intent.EXTRA_SUBJECT, subject);
30 | intent.putExtra(Intent.EXTRA_TEXT, content);
31 |
32 | context.startActivity(intent);
33 | }
34 |
35 | public static String generateReport(Exception e, Activity activity, String extra) {
36 | String[] errorMessage = {e.getMessage(), e.toString()};
37 |
38 | return generateReport(errorMessage, activity, extra);
39 | }
40 |
41 | public static String generateReport(String[] errorMessage, Activity activity, String extra) {
42 | String message = "Feel free to add a message explaining the circumstances of the error below, I'll do my best to help you:\n\n\n\n";
43 |
44 | message += "-------------\n";
45 | message += "Also make sure that your Shaarli instance is correctly setup. Common causes for issues:\n";
46 | message += " - RestAPI not enabled in Shaarli settings\n";
47 | message += " - mod_rewrite not enabled on Apache\n";
48 | message += " - Using password authentication with a custom theme\n";
49 | message += " - Using a hosting provider that injects Javascript in webpage.\n\n";
50 |
51 | message += "Thanks for the report, I'll try to answer as soon as possible!\n\n";
52 |
53 | message += "Bugtracker: https://github.com/dimtion/Shaarlier/issues\n\n";
54 |
55 | message += "-------------\n";
56 |
57 | message += "Below this line is an automated report, if you don't feel comfortable sharing ";
58 | message += "some of these fields please remove them. This data is exclusively used for debugging.\n\n";
59 |
60 | message += "-----BEGIN REPORT-----\n";
61 | message += "Report type: ERROR \n";
62 | message += "Android version: " + " " + Build.VERSION.RELEASE + "\n";
63 | try {
64 | message += "App version: " + activity.getPackageManager()
65 | .getPackageInfo(activity.getPackageName(), 0).versionName + "\n";
66 | } catch (PackageManager.NameNotFoundException e1) {
67 | e1.printStackTrace();
68 | }
69 | message += "Activity: " + activity.toString() + "\n";
70 |
71 | TimeZone tz = TimeZone.getTimeZone("UTC");
72 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");
73 | df.setTimeZone(tz);
74 |
75 | message += "Date: " + df.format(new Date()) + "\n\n";
76 |
77 | for (String m : errorMessage) {
78 | message += m + "\n\n";
79 | }
80 |
81 | message += "-----EXTRA-----\n" + extra + "\n";
82 |
83 | message += "-----END REPORT-----\n\n";
84 | return message;
85 | }
86 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/helper/EncryptionHelper.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.helper;
2 |
3 | import android.util.Base64;
4 |
5 | import java.security.InvalidAlgorithmParameterException;
6 | import java.security.InvalidKeyException;
7 | import java.security.NoSuchAlgorithmException;
8 | import java.security.SecureRandom;
9 |
10 | import javax.crypto.BadPaddingException;
11 | import javax.crypto.Cipher;
12 | import javax.crypto.IllegalBlockSizeException;
13 | import javax.crypto.KeyGenerator;
14 | import javax.crypto.NoSuchPaddingException;
15 | import javax.crypto.SecretKey;
16 | import javax.crypto.spec.IvParameterSpec;
17 | import javax.crypto.spec.SecretKeySpec;
18 |
19 | /**
20 | * Created by dimtion on 13/05/2015.
21 | * A simple class to encrypt and decrypt simple data
22 | * (Probably needs review)
23 | */
24 | public class EncryptionHelper {
25 | public final static int KEY_LENGTH = 256;
26 | public final static int IV_LENGTH = 16;
27 |
28 | public static SecretKey generateKey() throws NoSuchAlgorithmException {
29 |
30 | SecureRandom secureRandom = new SecureRandom();
31 | // Do *not* seed secureRandom! Automatically seeded from system entropy.
32 | KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
33 | keyGenerator.init(KEY_LENGTH, secureRandom);
34 | return keyGenerator.generateKey();
35 | }
36 |
37 | public static byte[] generateInitialVector() {
38 | SecureRandom random = new SecureRandom();
39 | return random.generateSeed(IV_LENGTH);
40 | }
41 |
42 | public static String secretKeyToString(SecretKey secretKey) {
43 | return Base64.encodeToString(secretKey.getEncoded(), Base64.DEFAULT);
44 | }
45 |
46 | public static SecretKey stringToSecretKey(String stringKey) {
47 | byte[] encodedKey = Base64.decode(stringKey, Base64.DEFAULT);
48 | return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
49 | }
50 |
51 | private static byte[] encryptDecrypt(int mode, byte[] clear, SecretKey key, byte[] initialVector)
52 | throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
53 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
54 | IvParameterSpec ivParameterSpec = new IvParameterSpec(initialVector);
55 |
56 | cipher.init(mode, key, ivParameterSpec);
57 | return cipher.doFinal(clear);
58 | }
59 |
60 | public static byte[] encrypt(byte[] clear, SecretKey key, byte[] initialVector)
61 | throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
62 | return encryptDecrypt(Cipher.ENCRYPT_MODE, clear, key, initialVector);
63 | }
64 |
65 | public static byte[] decrypt(byte[] encrypted, SecretKey key, byte[] initialVector)
66 | throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
67 | return encryptDecrypt(Cipher.DECRYPT_MODE, encrypted, key, initialVector);
68 | }
69 |
70 | public static byte[] stringToBase64(String clear) {
71 | return Base64.encode(clear.getBytes(), Base64.DEFAULT);
72 | }
73 |
74 | public static String base64ToString(byte[] data) {
75 | return new String(Base64.decode(data, Base64.DEFAULT));
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/helper/ShareIntentParser.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.helper;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.util.Log;
6 |
7 | import androidx.core.app.ShareCompat;
8 |
9 | import com.dimtion.shaarlier.exceptions.UnsupportedIntent;
10 | import com.dimtion.shaarlier.models.Link;
11 | import com.dimtion.shaarlier.models.ShaarliAccount;
12 | import com.dimtion.shaarlier.network.NetworkUtils;
13 | import com.dimtion.shaarlier.utils.UserPreferences;
14 |
15 | import java.util.Objects;
16 |
17 | public class ShareIntentParser {
18 |
19 | private static final String LOGGER_NAME = "ShareIntentParser";
20 |
21 | private static final String EXTRA_TAGS = "tags";
22 | private static final String EXTRA_DESCRIPTION = "description";
23 |
24 | private static final String TEXT_MIME_TYPE = "text/plain";
25 |
26 | private final UserPreferences userPreferences;
27 | private final ShaarliAccount shaarliAccount;
28 |
29 | public ShareIntentParser(final ShaarliAccount shaarliAccount, final UserPreferences userPreferences) {
30 | this.userPreferences = userPreferences;
31 | this.shaarliAccount = shaarliAccount;
32 | }
33 |
34 | /**
35 | * Create a populated {@link Link} from an {@link Intent}.
36 | *
37 | * @param activity source activity
38 | * @param intent intent to parse
39 | * @return a populated Link
40 | * @throws UnsupportedIntent if throwing an unsupported intent.
41 | */
42 | public Link parse(final Activity activity, final Intent intent) throws UnsupportedIntent {
43 | if (!Intent.ACTION_SEND.equals(intent.getAction()) || !TEXT_MIME_TYPE.equals(intent.getType())) {
44 | Log.e(LOGGER_NAME, "Unsupported action " + intent.getAction() + " or mime type " + intent.getType());
45 | throw new UnsupportedIntent();
46 | }
47 |
48 | final ShareCompat.IntentReader reader = ShareCompat.IntentReader.from(activity);
49 |
50 | final String url = extractUrl(reader.getText().toString());
51 | final String title = userPreferences.isAutoTitle() ? extractTitle(reader.getSubject()) : "";
52 | final String defaultTags = intent.getStringExtra(EXTRA_TAGS) != null ?
53 | intent.getStringExtra(EXTRA_TAGS) : "";
54 | final String description;
55 | if (userPreferences.isAutoDescription() && Objects.nonNull(intent.getStringExtra(EXTRA_DESCRIPTION))) {
56 | description = intent.getStringExtra(EXTRA_DESCRIPTION);
57 | } else {
58 | description = "";
59 | }
60 |
61 | return new Link(
62 | url,
63 | title,
64 | description,
65 | defaultTags,
66 | userPreferences.isPrivateShare(),
67 | shaarliAccount,
68 | userPreferences.isTweet(),
69 | userPreferences.isToot(),
70 | null,
71 | null
72 | );
73 | }
74 |
75 | /**
76 | * Extract an url located in a text
77 | *
78 | * @param text: a text containing an url
79 | * @return url present in the input text
80 | */
81 | private String extractUrl(final String text) {
82 | String finalUrl;
83 |
84 | // Trim the url because for annoying apps that send to much data:
85 | finalUrl = text.trim();
86 |
87 | String[] possible_urls = finalUrl.split(" ");
88 |
89 | for (String url : possible_urls) {
90 | if (NetworkUtils.isUrl(url)) {
91 | finalUrl = url;
92 | break;
93 | }
94 | }
95 |
96 | finalUrl = finalUrl.substring(finalUrl.lastIndexOf(" ") + 1);
97 | finalUrl = finalUrl.substring(finalUrl.lastIndexOf("\n") + 1);
98 |
99 | // If the url is incomplete:
100 | if (NetworkUtils.isUrl("http://" + finalUrl) && !NetworkUtils.isUrl(finalUrl)) {
101 | finalUrl = "http://" + finalUrl;
102 | }
103 | // Delete trackers:
104 | if (finalUrl.contains("&utm_source=")) {
105 | finalUrl = finalUrl.substring(0, finalUrl.indexOf("&utm_source="));
106 | }
107 | if (finalUrl.contains("?utm_source=")) {
108 | finalUrl = finalUrl.substring(0, finalUrl.indexOf("?utm_source="));
109 | }
110 | if (finalUrl.contains("#xtor=RSS-")) {
111 | finalUrl = finalUrl.substring(0, finalUrl.indexOf("#xtor=RSS-"));
112 | }
113 |
114 | return finalUrl;
115 | }
116 |
117 | /**
118 | * Extract the title from a subject line
119 | *
120 | * @param subject: intent subject
121 | * @return Title
122 | */
123 | private String extractTitle(final String subject) {
124 | if (subject != null && !NetworkUtils.isUrl(subject)) {
125 | return subject;
126 | }
127 |
128 | return "";
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/models/Link.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.models;
2 |
3 | import java.io.Serializable;
4 | import java.util.Arrays;
5 | import java.util.List;
6 |
7 | public class Link implements Serializable {
8 | private Integer id;
9 | private String url;
10 | private String title;
11 | private String description;
12 | private String tags;
13 | private boolean isPrivate;
14 | private ShaarliAccount account;
15 | private boolean tweet;
16 | private boolean toot;
17 |
18 | private String datePostLink;
19 | private String token;
20 |
21 | public Link(String url, String title, String description, String tags, boolean isPrivate, ShaarliAccount account, boolean tweet, boolean toot, String datePostLink, String token) {
22 | this.url = url;
23 | this.title = title;
24 | this.description = description;
25 | this.tags = tags;
26 | this.isPrivate = isPrivate;
27 | this.account = account;
28 | this.tweet = tweet;
29 | this.toot = toot;
30 | this.datePostLink = datePostLink;
31 | this.token = token;
32 | }
33 |
34 | public Link(Link other) {
35 | this.id = other.id;
36 | this.url = other.url;
37 | this.title = other.title;
38 | this.description = other.description;
39 | this.tags = other.tags;
40 | this.isPrivate = other.isPrivate;
41 | this.account = other.account;
42 | this.tweet = other.tweet;
43 | this.toot = other.toot;
44 | this.datePostLink = other.datePostLink;
45 | this.token = other.token;
46 | }
47 |
48 | public Integer getId() {
49 | return id;
50 | }
51 |
52 | public void setId(int id) {
53 | this.id = id;
54 | }
55 |
56 | public String getUrl() {
57 | return url;
58 | }
59 |
60 | public void setUrl(String url) {
61 | this.url = url;
62 | }
63 |
64 | public String getTitle() {
65 | return title;
66 | }
67 |
68 | public void setTitle(String title) {
69 | this.title = title;
70 | }
71 |
72 | public String getDescription() {
73 | return description;
74 | }
75 |
76 | public void setDescription(String description) {
77 | this.description = description;
78 | }
79 |
80 | @Deprecated
81 | public String getTags() {
82 | return tags;
83 | }
84 |
85 | public List getTagList() {
86 | return Arrays.asList(tags.split(", "));
87 | }
88 |
89 | public void setTags(String tags) {
90 | this.tags = tags;
91 | }
92 |
93 | public boolean isPrivate() {
94 | return isPrivate;
95 | }
96 |
97 | public void setPrivate(boolean aPrivate) {
98 | isPrivate = aPrivate;
99 | }
100 |
101 | public ShaarliAccount getAccount() {
102 | return account;
103 | }
104 |
105 | public void setAccount(ShaarliAccount account) {
106 | this.account = account;
107 | }
108 |
109 | public boolean isTweet() {
110 | return tweet;
111 | }
112 |
113 | public void setTweet(boolean tweet) {
114 | this.tweet = tweet;
115 | }
116 |
117 | public boolean isToot() {
118 | return toot;
119 | }
120 |
121 | public void setToot(boolean toot) {
122 | this.toot = toot;
123 | }
124 |
125 | public String getDatePostLink() {
126 | return datePostLink;
127 | }
128 |
129 | public void setDatePostLink(String datePostLink) {
130 | this.datePostLink = datePostLink;
131 | }
132 |
133 | public String getToken() {
134 | return token;
135 | }
136 |
137 | public void setToken(String token) {
138 | this.token = token;
139 | }
140 |
141 | public boolean seemsNotNew() {
142 | return (
143 | (description != null && !description.trim().equals(""))
144 | || (tags != null && !tags.trim().equals(""))
145 | || id != null
146 | );
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/models/ShaarliAccount.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.models;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import java.io.Serializable;
6 |
7 | /**
8 | * Created by dimtion on 11/05/2015.
9 | * A Shaarli Account
10 | */
11 |
12 | public class ShaarliAccount implements Serializable {
13 | public static final int AUTH_METHOD_AUTO = 0;
14 | public static final int AUTH_METHOD_PASSWORD = 1;
15 | public static final int AUTH_METHOD_RESTAPI = 2;
16 | public static final int AUTH_METHOD_MOCK = 3;
17 |
18 | private long id;
19 | private String urlShaarli;
20 | private String username;
21 | private String password;
22 | private String basicAuthUsername;
23 | private String basicAuthPassword;
24 | private String shortName;
25 | private String restAPIKey;
26 | private byte[] initialVector;
27 | private boolean validateCert;
28 | private int authMethod = AUTH_METHOD_AUTO;
29 |
30 | @NonNull
31 | @Override
32 | public String toString() {
33 | if (this.shortName != null && !this.shortName.equals("")) {
34 | return shortName;
35 | } else {
36 | return this.urlShaarli;
37 | }
38 | }
39 |
40 | public long getId() {
41 | return id;
42 | }
43 |
44 | public void setId(long id) {
45 | this.id = id;
46 | }
47 |
48 | public String getUrlShaarli() {
49 | return urlShaarli;
50 | }
51 |
52 | public void setUrlShaarli(String urlShaarli) {
53 | this.urlShaarli = urlShaarli;
54 | }
55 |
56 | public String getUsername() {
57 | return username;
58 | }
59 |
60 | public void setUsername(String username) {
61 | this.username = username;
62 | }
63 |
64 | public String getPassword() {
65 | return password;
66 | }
67 |
68 | public void setPassword(String password) {
69 | this.password = password;
70 | }
71 |
72 | public String getBasicAuthUsername() {
73 | return basicAuthUsername;
74 | }
75 |
76 | public void setBasicAuthUsername(String basicAuthUsername) {
77 | this.basicAuthUsername = basicAuthUsername;
78 | }
79 |
80 | public String getBasicAuthPassword() {
81 | return basicAuthPassword;
82 | }
83 |
84 | public void setBasicAuthPassword(String basicAuthPassword) {
85 | this.basicAuthPassword = basicAuthPassword;
86 | }
87 |
88 | public String getShortName() {
89 | return shortName;
90 | }
91 |
92 | public void setShortName(String shortName) {
93 | this.shortName = shortName;
94 | }
95 |
96 | public byte[] getInitialVector() {
97 | return initialVector;
98 | }
99 |
100 | public void setInitialVector(byte[] initialVector) {
101 | this.initialVector = initialVector;
102 | }
103 |
104 | public boolean isValidateCert() {
105 | return validateCert;
106 | }
107 |
108 | public void setValidateCert(boolean validateCert) {
109 | this.validateCert = validateCert;
110 | }
111 |
112 | public String getRestAPIKey() {
113 | return restAPIKey;
114 | }
115 |
116 | public void setRestAPIKey(String restAPIKey) {
117 | this.restAPIKey = restAPIKey;
118 | }
119 |
120 | public int getAuthMethod() {
121 | return authMethod;
122 | }
123 |
124 | public void setAuthMethod(int authMethod) {
125 | this.authMethod = authMethod;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/models/Tag.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.models;
2 |
3 | /**
4 | * Created by dimtion on 12/05/2015.
5 | * A tag from the SQLite db
6 | */
7 | public class Tag {
8 | private long id;
9 | private ShaarliAccount masterAccount;
10 | private String value;
11 | private long masterAccountId;
12 |
13 | public long getId() {
14 | return id;
15 | }
16 |
17 | public void setId(long id) {
18 | this.id = id;
19 | }
20 |
21 | public ShaarliAccount getMasterAccount() {
22 | return masterAccount;
23 | }
24 |
25 | public void setMasterAccount(ShaarliAccount masterAccount) {
26 | this.masterAccount = masterAccount;
27 | this.masterAccountId = masterAccount.getId();
28 | }
29 |
30 | public String getValue() {
31 | return value;
32 | }
33 |
34 | public void setValue(String value) {
35 | this.value = value;
36 | }
37 |
38 | public long getMasterAccountId() {
39 | return masterAccountId;
40 | }
41 |
42 | public void setMasterAccountId(long masterAccountId) {
43 | this.masterAccountId = masterAccountId;
44 | }
45 |
46 | @Override
47 | public String toString() {
48 | return getValue();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/network/MockNetworkManager.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.network;
2 |
3 | import com.dimtion.shaarlier.models.Link;
4 |
5 | import java.io.IOException;
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | /**
10 | * Dummy NetworkManager used for debugging purposes
11 | */
12 | public class MockNetworkManager implements NetworkManager {
13 | @Override
14 | public boolean isCompatibleShaarli() throws IOException {
15 | return true;
16 | }
17 |
18 | @Override
19 | public boolean login() throws IOException {
20 | return true;
21 | }
22 |
23 | @Override
24 | public Link prefetchLinkData(Link link) throws IOException {
25 | return link;
26 | }
27 |
28 | @Override
29 | public void pushLink(Link link) {
30 | }
31 |
32 | @Override
33 | public List retrieveTags() throws Exception {
34 | ArrayList tags = new ArrayList<>();
35 | tags.add("test-tag1");
36 | tags.add("test-tag2");
37 | return tags;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/network/NetworkManager.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.network;
2 |
3 | import com.dimtion.shaarlier.models.Link;
4 |
5 | import java.io.IOException;
6 | import java.util.List;
7 |
8 | public interface NetworkManager {
9 | /**
10 | * returns whether the distant Shaarli server is compatible with this network manager
11 | *
12 | * @return true if compatible, false otherwise
13 | * @throws IOException
14 | */
15 | boolean isCompatibleShaarli() throws IOException;
16 |
17 | /**
18 | * Check if the provided credentials are valid
19 | * Can set some state in the NetworkManager (like a cookie if necessary)
20 | *
21 | * @return true if the credentials are correct, false otherwise
22 | * @throws IOException
23 | */
24 | boolean login() throws IOException;
25 |
26 | Link prefetchLinkData(Link link) throws IOException;
27 |
28 | void pushLink(Link link) throws IOException;
29 |
30 | List retrieveTags() throws Exception;
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/network/NetworkUtils.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.network;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.net.ConnectivityManager;
6 | import android.net.NetworkInfo;
7 | import android.util.Log;
8 | import android.webkit.URLUtil;
9 |
10 | import androidx.annotation.NonNull;
11 |
12 | import com.dimtion.shaarlier.models.ShaarliAccount;
13 |
14 | import org.jsoup.Jsoup;
15 | import org.jsoup.nodes.Document;
16 |
17 | public abstract class NetworkUtils {
18 | protected static final int TIME_OUT = 60_000; // Better for mobile connections
19 |
20 | private final static String LOGGER_NAME = NetworkUtils.class.getSimpleName();
21 |
22 | private static final String[] DESCRIPTION_SELECTORS = {
23 | "meta[property=og:description]",
24 | "meta[name=description]",
25 | "meta[name=twitter:description]",
26 | "meta[name=mastodon:description]",
27 | };
28 |
29 | /**
30 | * Check if a string is an url
31 | * TODO : unit test on this, I'm not quite sure it is perfect...
32 | */
33 | public static boolean isUrl(String url) {
34 | return URLUtil.isValidUrl(url) && !"http://".equals(url);
35 | }
36 |
37 | /**
38 | * Change something which is close to a url to something that is really one
39 | */
40 | public static String toUrl(String givenUrl) {
41 | String finalUrl = givenUrl;
42 | String protocol = "http://"; // Default value
43 | if ("".equals(givenUrl)) {
44 | return givenUrl; // Edge case, maybe need some discussion
45 | }
46 |
47 | if (!finalUrl.endsWith("/")) {
48 | finalUrl += '/';
49 | }
50 |
51 | if (!(finalUrl.startsWith("http://") || finalUrl.startsWith("https://"))) {
52 | finalUrl = protocol + finalUrl;
53 | }
54 |
55 | return finalUrl;
56 | }
57 |
58 | /**
59 | * Method to test the network connection
60 | *
61 | * @return true if the device is connected to the network
62 | */
63 | public static boolean testNetwork(@NonNull Activity parentActivity) {
64 | ConnectivityManager connMgr = (ConnectivityManager) parentActivity.getSystemService(Context.CONNECTIVITY_SERVICE);
65 | NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
66 | return (networkInfo != null && networkInfo.isConnected());
67 | }
68 |
69 | /**
70 | * Static method to load the title of a web page
71 | *
72 | * @param url the url of the web page
73 | * @return "" if there is an error, the page title in other cases
74 | */
75 | public static String[] loadTitleAndDescription(@NonNull String url) {
76 | String title = "";
77 | String description = "";
78 | final Document pageResp;
79 | try {
80 | Log.i(LOGGER_NAME, "Loading url: " + url);
81 | pageResp = Jsoup.connect(url)
82 | .followRedirects(true)
83 | .execute()
84 | .parse();
85 | title = pageResp.title();
86 | } catch (final Exception e) {
87 | // Just abandon the task if there is a problem
88 | Log.e(LOGGER_NAME, "Failed to load title: " + e);
89 | return new String[]{title, description};
90 | }
91 |
92 | // Many ways to get the description
93 | for (String selector : NetworkUtils.DESCRIPTION_SELECTORS) {
94 | try {
95 | description = pageResp.head().select(selector).first().attr("content");
96 | } catch (final Exception e) {
97 | Log.e(LOGGER_NAME, "Failed to load description: " + e);
98 | }
99 | if (!"".equals(description)) {
100 | break;
101 | }
102 | }
103 | return new String[]{title, description};
104 | }
105 |
106 | /**
107 | * Select the correct network manager based on the passed account
108 | */
109 | public static NetworkManager getNetworkManager(ShaarliAccount account) {
110 | switch (account.getAuthMethod()) {
111 | case ShaarliAccount.AUTH_METHOD_MOCK:
112 | Log.i(LOGGER_NAME, "Selected MockNetworkManager (forced)");
113 | return new MockNetworkManager();
114 | case ShaarliAccount.AUTH_METHOD_PASSWORD:
115 | Log.i(LOGGER_NAME, "Selected PasswordNetworkManager (forced)");
116 | return new PasswordNetworkManager(account);
117 | case ShaarliAccount.AUTH_METHOD_RESTAPI:
118 | Log.i(LOGGER_NAME, "Selected RestAPiNetworkManager (forced)");
119 | return new RestAPINetworkManager(account);
120 | case ShaarliAccount.AUTH_METHOD_AUTO:
121 | if (1 == 0) { // Enabled only for debugging purposes
122 | Log.i(LOGGER_NAME, "Selected MockNetworkManager (auto)");
123 | return new MockNetworkManager();
124 | }
125 |
126 | if (account.getRestAPIKey() != null && account.getRestAPIKey().length() > 0) {
127 | Log.i(LOGGER_NAME, "Selected RestAPiNetworkManager (auto)");
128 | return new RestAPINetworkManager(account);
129 | } else {
130 | Log.i(LOGGER_NAME, "Selected PasswordNetworkManager (auto)");
131 | return new PasswordNetworkManager(account);
132 | }
133 | default:
134 | throw new RuntimeException("Invalid shaarli auth method");
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/network/PasswordNetworkManager.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.network;
2 |
3 | import android.util.Base64;
4 | import android.util.Log;
5 |
6 | import androidx.annotation.NonNull;
7 |
8 | import com.dimtion.shaarlier.models.Link;
9 | import com.dimtion.shaarlier.models.ShaarliAccount;
10 |
11 | import org.json.JSONArray;
12 | import org.json.JSONException;
13 | import org.jsoup.Connection;
14 | import org.jsoup.Jsoup;
15 | import org.jsoup.nodes.Element;
16 |
17 | import java.io.IOException;
18 | import java.net.URLEncoder;
19 | import java.util.ArrayList;
20 | import java.util.Collections;
21 | import java.util.List;
22 | import java.util.Map;
23 |
24 | /**
25 | * Created by dimtion on 05/04/2015.
26 | * This class handle all the communications with Shaarli and other web services
27 | */
28 | public class PasswordNetworkManager implements NetworkManager {
29 |
30 | private final String mShaarliUrl;
31 | private final String mUsername;
32 | private final String mPassword;
33 | private final boolean mValidateCert;
34 | private final String mBasicAuth;
35 |
36 | private Map mCookies;
37 | private String mToken;
38 |
39 | private String mDatePostLink;
40 | private String mSharedUrl;
41 | private Exception mLastError;
42 |
43 | private String mPrefetchedTitle;
44 | private String mPrefetchedDescription;
45 | private String mPrefetchedTags;
46 |
47 | PasswordNetworkManager(@NonNull ShaarliAccount account) {
48 | this.mShaarliUrl = account.getUrlShaarli();
49 | this.mUsername = account.getUsername();
50 | this.mPassword = account.getPassword();
51 | this.mValidateCert = account.isValidateCert();
52 |
53 | if (!"".equals(account.getBasicAuthUsername()) && !"".equals(account.getBasicAuthPassword())) {
54 | String login = account.getBasicAuthUsername() + ":" + account.getBasicAuthPassword();
55 | this.mBasicAuth = new String(Base64.encode(login.getBytes(), Base64.NO_WRAP));
56 | } else {
57 | this.mBasicAuth = "";
58 | }
59 | }
60 |
61 | /**
62 | * Helper method which create a new connection to Shaarli
63 | *
64 | * @param url the url of the shaarli
65 | * @param method set the HTTP method to use
66 | * @return pre-made jsoupConnection
67 | */
68 | private Connection newConnection(String url, Connection.Method method) {
69 | Connection jsoupConnection = Jsoup.connect(url);
70 |
71 | if (!"".equals(this.mBasicAuth)) {
72 | jsoupConnection = jsoupConnection.header("Authorization", "Basic " + this.mBasicAuth);
73 | }
74 | if (this.mCookies != null) {
75 | jsoupConnection = jsoupConnection.cookies(this.mCookies);
76 | }
77 |
78 | return jsoupConnection
79 | .validateTLSCertificates(this.mValidateCert)
80 | .timeout(NetworkUtils.TIME_OUT)
81 | .followRedirects(true)
82 | .method(method);
83 | }
84 |
85 | @Override
86 | public boolean isCompatibleShaarli() throws IOException {
87 | final String loginFormUrl = this.mShaarliUrl + "?do=login";
88 | try {
89 | Connection.Response loginFormPage = this.newConnection(loginFormUrl, Connection.Method.GET).execute();
90 | this.mCookies = loginFormPage.cookies();
91 | this.mToken = loginFormPage.parse().body().select("input[name=token]").first().attr("value");
92 | } catch (NullPointerException | IllegalArgumentException e) {
93 | Log.e("PasswordNetworkManager", "incompatible shaarli: " + e);
94 | return false;
95 | }
96 | Log.i("PasswordNetworkManager", "Compatible shaarli: ");
97 | return true;
98 | }
99 |
100 | /**
101 | * Method which retrieve the cookie saying that we are logged in
102 | */
103 | @Override
104 | public boolean login() throws IOException {
105 | try {
106 | Connection.Response loginPage = this.newConnection(this.mShaarliUrl, Connection.Method.POST)
107 | .data("login", this.mUsername)
108 | .data("password", this.mPassword)
109 | .data("token", this.mToken)
110 | .data("returnurl", this.mShaarliUrl)
111 | .execute();
112 |
113 | this.mCookies = loginPage.cookies();
114 | loginPage.parse().body().select("a[href=?do=logout]").first()
115 | .attr("href"); // If this fails, you're not connected
116 |
117 | } catch (NullPointerException e) {
118 | Log.e("PasswordNetworkManager", "login error: " + e);
119 | return false;
120 | }
121 | return true;
122 | }
123 |
124 | /**
125 | * Method which retrieve a token for posting links
126 | * Update the cookie, the token and the date
127 | * Assume being logged in
128 | */
129 | private void retrievePostLinkToken(String encodedSharedLink) throws IOException {
130 | final String postFormUrl = this.mShaarliUrl + "?post=" + encodedSharedLink;
131 |
132 | Connection.Response postFormPage = this.newConnection(postFormUrl, Connection.Method.GET)
133 | .execute();
134 | final Element postFormBody = postFormPage.parse().body();
135 |
136 | // Update our situation:
137 | // TODO: Soft fail: if one field does not load, try the others anyway
138 | mToken = postFormBody.select("input[name=token]").first().attr("value");
139 | mDatePostLink = postFormBody.select("input[name=lf_linkdate]").first().attr("value"); // Date chosen by the server
140 | mSharedUrl = postFormBody.select("input[name=lf_url]").first().attr("value");
141 | mPrefetchedTitle = postFormBody.select("input[name=lf_title]").first().attr("value");
142 | mPrefetchedDescription = postFormBody.select("textarea[name=lf_description]").first().html();
143 | mPrefetchedTags = postFormBody.select("input[name=lf_tags]").first().attr("value");
144 | }
145 |
146 | @Override
147 | public Link prefetchLinkData(Link link) throws IOException {
148 | String encodedShareUrl = URLEncoder.encode(link.getUrl(), "UTF-8");
149 | retrievePostLinkToken(encodedShareUrl);
150 |
151 | Link newLink = new Link(link);
152 | newLink.setUrl(mSharedUrl);
153 | newLink.setTitle(mPrefetchedTitle);
154 | newLink.setDescription(mPrefetchedDescription);
155 | newLink.setTags(mPrefetchedTags);
156 | newLink.setDatePostLink(mDatePostLink);
157 | newLink.setToken(mToken);
158 | return newLink;
159 | }
160 |
161 | /**
162 | * Method which publishes a link to shaarli
163 | * Assume being logged in
164 | * TODO: use the prefetch function
165 | */
166 | @Override
167 | public void pushLink(Link link) throws IOException {
168 | String encodedShareUrl = URLEncoder.encode(link.getUrl(), "UTF-8");
169 | retrievePostLinkToken(encodedShareUrl);
170 |
171 | if (NetworkUtils.isUrl(link.getUrl())) { // In case the url isn't really one, just post the one chosen by the server.
172 | this.mSharedUrl = link.getUrl();
173 | }
174 |
175 | final String postUrl = this.mShaarliUrl + "?post=" + encodedShareUrl;
176 |
177 | Connection postPageConn = this.newConnection(postUrl, Connection.Method.POST)
178 | .data("save_edit", "Save")
179 | .data("token", this.mToken)
180 | .data("lf_tags", link.getTags())
181 | .data("lf_linkdate", this.mDatePostLink)
182 | .data("lf_url", this.mSharedUrl)
183 | .data("lf_title", link.getTitle())
184 | .data("lf_description", link.getDescription());
185 | if (link.isPrivate()) postPageConn.data("lf_private", "on");
186 | if (link.isTweet()) postPageConn.data("tweet", "on");
187 | if (link.isToot()) postPageConn.data("toot", "on");
188 | postPageConn.execute(); // Then we post
189 | }
190 |
191 | @Override
192 | public List retrieveTags() {
193 | List tags = new ArrayList<>();
194 | try {
195 | String[] awesompleteTags = this.retrieveTagsFromAwesomplete();
196 | Collections.addAll(tags, awesompleteTags);
197 | } catch (Exception e) {
198 | Log.w("TAG", e.toString());
199 | }
200 | try {
201 | String[] wsTags = this.retrieveTagsFromWs(); // kept for compatibility with old Shaarli instances
202 | Collections.addAll(tags, wsTags);
203 | } catch (Exception e) {
204 | Log.w("TAG", e.toString());
205 | }
206 | return tags;
207 | }
208 |
209 | /**
210 | * Method which retrieve tags from the WS (old shaarli)
211 | * Assume being logged in
212 | */
213 | private String[] retrieveTagsFromWs() throws IOException, JSONException {
214 | final String requestUrl = this.mShaarliUrl + "?ws=tags&term=+";
215 | String[] predictionsArr = {};
216 | String json = this.newConnection(requestUrl, Connection.Method.POST)
217 | .ignoreContentType(true)
218 | .execute()
219 | .body();
220 |
221 | JSONArray ja = new JSONArray(json);
222 | predictionsArr = new String[ja.length()];
223 | for (int i = 0; i < ja.length(); i++) {
224 | // add each entry to our array
225 | predictionsArr[i] = ja.getString(i);
226 | }
227 |
228 | return predictionsArr;
229 | }
230 |
231 | /**
232 | * Method which retrieve tags from awesomplete (new shaarli)
233 | * Assume being logged in
234 | */
235 | private String[] retrieveTagsFromAwesomplete() throws IOException {
236 | final String requestUrl = this.mShaarliUrl + "?post=";
237 | String tagsString = this.newConnection(requestUrl, Connection.Method.GET)
238 | .execute()
239 | .parse()
240 | .body()
241 | .select("input[name=lf_tags]")
242 | .first()
243 | .attr("data-list");
244 | return tagsString.split(", ");
245 | }
246 | }
247 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/network/RestAPINetworkManager.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.network;
2 |
3 | import android.util.Log;
4 |
5 | import androidx.annotation.NonNull;
6 |
7 | import com.dimtion.shaarlier.models.Link;
8 | import com.dimtion.shaarlier.models.ShaarliAccount;
9 |
10 | import org.json.JSONArray;
11 | import org.json.JSONException;
12 | import org.json.JSONObject;
13 | import org.jsoup.Connection;
14 | import org.jsoup.HttpStatusException;
15 | import org.jsoup.Jsoup;
16 |
17 | import java.io.IOException;
18 | import java.net.URL;
19 | import java.util.ArrayList;
20 | import java.util.Date;
21 | import java.util.List;
22 |
23 | import io.jsonwebtoken.Jwts;
24 | import io.jsonwebtoken.SignatureAlgorithm;
25 |
26 | public class RestAPINetworkManager implements NetworkManager {
27 | private static final String TAGS_URL = "api/v1/tags";
28 | private static final String INFO_URL = "api/v1/info";
29 | private static final String LINK_URL = "api/v1/links";
30 |
31 | private final static String LOGGER_NAME = NetworkUtils.class.getSimpleName();
32 |
33 | private final ShaarliAccount mAccount;
34 |
35 | RestAPINetworkManager(@NonNull ShaarliAccount account) {
36 | this.mAccount = account;
37 | }
38 |
39 | @Override
40 | public boolean isCompatibleShaarli() throws IOException {
41 | String url = new URL(this.mAccount.getUrlShaarli() + INFO_URL).toExternalForm();
42 | Log.i(LOGGER_NAME, "Check Shaarli compatibility");
43 | try {
44 | final String body = this.newConnection(url, Connection.Method.GET)
45 | .execute()
46 | .body();
47 | Log.d(LOGGER_NAME, "JWT used: " + body);
48 | final JSONObject resp = new JSONObject(body);
49 |
50 | // For example check that the settings var exists
51 | if (resp.getJSONObject("settings") == null) {
52 | Log.i(LOGGER_NAME, "Settings var does not exists");
53 | return false;
54 | }
55 | } catch (final HttpStatusException e) {
56 | Log.w(LOGGER_NAME, "Exception while calling Shaarli: " + e.getMessage(), e);
57 | if (e.getStatusCode() == 404) {
58 | Log.i(LOGGER_NAME, "API V1 not supported");
59 | return false; // API V1 not supported
60 | } else {
61 | return e.getStatusCode() == 401; // API V1 supported
62 | }
63 | } catch (final JSONException e) {
64 | Log.e(LOGGER_NAME, "JSONException while calling shaarli: " + e.getMessage(), e);
65 | return false;
66 | } catch (final IllegalArgumentException e) {
67 | // This exception arises in a bug in JWT module. I added that to help with the debugging
68 | Log.e(LOGGER_NAME, "Error generating JWT: " + e.getMessage(), e);
69 | throw new IOException("isCompatibleShaarli: " + e, e);
70 | }
71 | // assume a 2XX or 3XX means API V1 supported
72 | Log.i(LOGGER_NAME, "API V1 supported");
73 | return true;
74 | }
75 |
76 | @Override
77 | public boolean login() throws IOException {
78 | // TODO: we could set some account parameters from here like default_private_links
79 | String url = new URL(this.mAccount.getUrlShaarli() + INFO_URL).toExternalForm();
80 | try {
81 | Log.d("Login", this.mAccount.getRestAPIKey());
82 | String body = this.newConnection(url, Connection.Method.GET)
83 | .execute()
84 | .body();
85 | Log.i("Login", body);
86 | } catch (HttpStatusException e) {
87 | Log.w("Login", e);
88 | Log.w("Login", e.getMessage());
89 | return false;
90 | }
91 | return true;
92 | }
93 |
94 | @Override
95 | public Link prefetchLinkData(Link link) throws IOException {
96 | // TODO: There might be some bugs here, e.g:
97 | // - If the scheme used is not the same that on the saved link
98 | // - If there are tracking tags that don't match
99 | // We might want to open an Issue on Shaarli to get feedback
100 | String url = new URL(this.mAccount.getUrlShaarli() + LINK_URL).toExternalForm();
101 | String body = this.newConnection(url, Connection.Method.GET)
102 | .data("offset", "0")
103 | .data("limit", "1")
104 | .data("searchterm", link.getUrl())
105 | .execute()
106 | .body();
107 | Log.d("RestAPI:prefetch", body);
108 |
109 | Link updatedLink = new Link(link);
110 | try {
111 | JSONArray resp = new JSONArray(body);
112 | if (resp.length() < 1) {
113 | Log.i("RestAPI:prefetch", "New link");
114 | } else {
115 | Log.i("RestAPI:prefetch", "Found 1 link result (not new link)");
116 | JSONObject returnedLink = resp.getJSONObject(0);
117 | updatedLink.setUrl(returnedLink.getString("url"));
118 | updatedLink.setTitle(returnedLink.getString("title"));
119 | updatedLink.setDescription(returnedLink.getString("description"));
120 | updatedLink.setPrivate(returnedLink.getBoolean("private"));
121 | JSONArray jsonTags = returnedLink.getJSONArray("tags");
122 | ArrayList tags = new ArrayList<>();
123 | for (int i = 0; i < jsonTags.length(); i++) {
124 | tags.add(jsonTags.getString(i));
125 | }
126 | updatedLink.setTags(String.join(", ", tags));
127 | }
128 | } catch (JSONException e) {
129 | Log.e("RestAPI:prefetch", e.toString());
130 | }
131 | return updatedLink;
132 | }
133 |
134 | @Override
135 | public void pushLink(Link link) throws IOException {
136 | String url = new URL(link.getAccount().getUrlShaarli() + LINK_URL).toExternalForm();
137 |
138 | // Create the request body:
139 | JSONObject requestBody = new JSONObject();
140 | try {
141 | requestBody.put("url", link.getUrl());
142 | requestBody.put("title", link.getTitle());
143 | requestBody.put("description", link.getDescription());
144 | requestBody.put("tags", new JSONArray(link.getTagList()));
145 | requestBody.put("private", link.isPrivate());
146 | if (link.isTweet()) { // TODO tweet
147 | Log.e("RequestAPI:post", "Tweet feature not implemented");
148 | }
149 | if (link.isToot()) { // TODO toot
150 | Log.e("RequestAPI:post", "Toot feature not implemented");
151 | }
152 | Log.d("PushLink", requestBody.toString(2));
153 | } catch (JSONException e) {
154 | Log.e("RequestAPI:post", e.toString());
155 | }
156 |
157 | // 1. Try POST only if link id is null
158 | if (link.getId() == null) {
159 | Connection.Response resp = this.newConnection(url, Connection.Method.POST)
160 | .requestBody(requestBody.toString())
161 | .ignoreHttpErrors(true)
162 | .execute();
163 | if (200 <= resp.statusCode() && resp.statusCode() <= 201) { // HTTP Ok after redirect
164 | return; // We are done here
165 | } else if (resp.statusCode() != 409) { // 409 HTTP Conflict
166 | // every other error we bubble up
167 | throw new HttpStatusException("Error requesting: " + url + " : " + resp.statusCode(), resp.statusCode(), url);
168 | }
169 | // On conflict, we update our id
170 | try {
171 | link.setId(new JSONObject(resp.body()).getInt("id"));
172 | } catch (JSONException e) {
173 | throw new IOException("Invalid id sent by Shaarli: " + e);
174 | }
175 | }
176 |
177 | // 2. If POST failed or link had id try PUT:
178 | this.newConnection(url + "/" + link.getId(), Connection.Method.PUT)
179 | .requestBody(requestBody.toString())
180 | .ignoreHttpErrors(false)
181 | .execute();
182 | }
183 |
184 | /**
185 | * Helper method to a new connection to the shaarli instance
186 | *
187 | * @param url to connect to
188 | * @param method HTTP method
189 | * @return a new opened connection
190 | */
191 | private Connection newConnection(String url, Connection.Method method) {
192 | Log.i(LOGGER_NAME, "Creating new connection " + url + " : " + method);
193 | return Jsoup.connect(url)
194 | .header("Authorization", "Bearer " + this.getJwt())
195 | .header("Content-Type", "application/json")
196 | .ignoreContentType(true) // application/json
197 | .validateTLSCertificates(this.mAccount.isValidateCert())
198 | .timeout(NetworkUtils.TIME_OUT)
199 | .followRedirects(true)
200 | .method(method);
201 | }
202 |
203 | @Override
204 | public List retrieveTags() throws Exception {
205 | String url = new URL(this.mAccount.getUrlShaarli() + TAGS_URL).toExternalForm();
206 | String body = this.newConnection(url, Connection.Method.GET)
207 | .execute()
208 | .body();
209 | List tags = new ArrayList();
210 | JSONArray resp = new JSONArray(body);
211 | for (int i = 0; i < resp.length(); i++) {
212 | tags.add(resp.getJSONObject(i).getString("name"));
213 | }
214 | return tags;
215 | }
216 |
217 | /**
218 | * Inspired by gitlab
219 | * License: MIT
220 | * Copyright 2017 braincoke
221 | *
222 | * @return JWT encoded in base 64
223 | */
224 | String getJwt() {
225 | // TODO: we are obligated to stay with jjwt 0.9.1 because of Shaarli weak keys
226 |
227 | Log.i("JWT", "Generating JWT for account " + this.mAccount);
228 | // iat in the payload
229 | Date date = new Date();
230 | // During debugging I found that given that some servers and phones are not absolutely in sync
231 | // It happens that the token would looked like being generated in the future
232 | // To compensate that we remove 5 seconds from the actual date.
233 | date.setTime(date.getTime() - 5000);
234 | // The key used to sign the token, you can find it by logging to your Shaarli instance
235 | // and then going to "Tools"
236 | byte[] signingKey = this.mAccount.getRestAPIKey().getBytes();
237 | // We create the token with the Jwts library
238 | return Jwts.builder()
239 | .setIssuedAt(date)
240 | .setHeaderParam("typ", "JWT")
241 | .signWith(SignatureAlgorithm.HS512, signingKey)
242 | .compact();
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/services/NetworkService.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.services;
2 |
3 | import android.app.IntentService;
4 | import android.app.NotificationChannel;
5 | import android.app.NotificationManager;
6 | import android.app.PendingIntent;
7 | import android.content.Context;
8 | import android.content.Intent;
9 | import android.os.Build;
10 | import android.os.Handler;
11 | import android.os.Looper;
12 | import android.os.Message;
13 | import android.os.Messenger;
14 | import android.util.Log;
15 | import android.widget.Toast;
16 |
17 | import androidx.annotation.NonNull;
18 | import androidx.annotation.Nullable;
19 | import androidx.core.app.NotificationCompat;
20 | import androidx.core.app.TaskStackBuilder;
21 |
22 | import com.dimtion.shaarlier.R;
23 | import com.dimtion.shaarlier.models.Link;
24 | import com.dimtion.shaarlier.models.ShaarliAccount;
25 | import com.dimtion.shaarlier.network.NetworkManager;
26 | import com.dimtion.shaarlier.network.NetworkUtils;
27 |
28 | import java.io.IOException;
29 | import java.util.Arrays;
30 |
31 | public class NetworkService extends IntentService {
32 | public static final String EXTRA_MESSENGER = "com.dimtion.shaarlier.networkservice.EXTRA_MESSENGER";
33 | private static final String LOGGER_NAME = NetworkService.class.getSimpleName();
34 | public static final int NO_ERROR = 0;
35 | public static final int NETWORK_ERROR = 1;
36 | public static final int TOKEN_ERROR = 2;
37 | public static final int LOGIN_ERROR = 3;
38 |
39 | public static final int RETRIEVE_TITLE_ID = 100;
40 | public static final int PREFETCH_LINK = 101;
41 |
42 | public static final int INTENT_CHECK = 201;
43 | public static final int INTENT_POST = 202;
44 | public static final int INTENT_PREFETCH = 203;
45 | public static final int INTENT_RETRIEVE_TITLE_AND_DESCRIPTION = 204;
46 |
47 | // Notification channels
48 | public static final String CHANNEL_ID = "error_channel";
49 |
50 | private String loadedTitle;
51 |
52 | private Context mContext;
53 | private Handler mToastHandler;
54 | private Exception mError;
55 | private String loadedDescription;
56 |
57 | public NetworkService() {
58 | super("NetworkService");
59 | }
60 |
61 | @Override
62 | public void onCreate(){
63 | super.onCreate();
64 | mContext = this;
65 | mToastHandler = new Handler(Looper.getMainLooper());
66 | this.createNotificationChannel();
67 | }
68 |
69 | @Override
70 | protected void onHandleIntent(Intent intent) {
71 | int action = intent.getIntExtra("action", -1);
72 |
73 | switch (action) {
74 | case INTENT_CHECK:
75 | ShaarliAccount accountToTest = (ShaarliAccount) intent.getSerializableExtra("account");
76 |
77 | int shaarliLstatus = checkShaarli(accountToTest);
78 | Exception object = shaarliLstatus == NETWORK_ERROR ? mError : null;
79 | sendBackMessage(intent, shaarliLstatus, object);
80 | break;
81 |
82 | case INTENT_POST:
83 | Link link = (Link) intent.getSerializableExtra("link");
84 |
85 | if ("".equals(link.getTitle()) && this.loadedTitle != null) {
86 | link.setTitle(this.loadedTitle);
87 | this.loadedTitle = null;
88 | }
89 | if ("".equals(link.getDescription()) && this.loadedDescription != null) {
90 | link.setDescription(this.loadedDescription);
91 | this.loadedDescription = null;
92 | }
93 | postLink(link);
94 | stopSelf();
95 | break;
96 |
97 | case INTENT_PREFETCH:
98 | Link sharedLink = (Link) intent.getSerializableExtra("link");
99 | Link prefetchedLink = prefetchLink(sharedLink);
100 | sendBackMessage(intent, PREFETCH_LINK, prefetchedLink);
101 | break;
102 |
103 | case INTENT_RETRIEVE_TITLE_AND_DESCRIPTION:
104 | this.loadedTitle = "";
105 | this.loadedDescription = "";
106 |
107 | final String url = intent.getStringExtra("url");
108 |
109 | boolean autoTitle = intent.getBooleanExtra("autoTitle", true);
110 | boolean autoDescription = intent.getBooleanExtra("autoDescription", false);
111 |
112 | final String[] pageTitleAndDescription = getPageTitleAndDescription(url);
113 | Log.i(LOGGER_NAME, "Title and description: " + Arrays.toString(pageTitleAndDescription));
114 |
115 | if (autoTitle) {
116 | this.loadedTitle = pageTitleAndDescription[0];
117 | }
118 | if (autoDescription) {
119 | this.loadedDescription = pageTitleAndDescription[1];
120 | }
121 |
122 | sendBackMessage(intent, RETRIEVE_TITLE_ID, pageTitleAndDescription);
123 | break;
124 | default:
125 | // Do nothing
126 | Log.e("NETWORK_ERROR", "Unknown intent action received: " + action);
127 | break;
128 | }
129 | }
130 |
131 | private void sendBackMessage(@NonNull Intent intent, int message_id, @Nullable Object message_content) {
132 | // Load the messenger to communicate back to the activity
133 | Messenger messenger = (Messenger) intent.getExtras().get(EXTRA_MESSENGER);
134 | Message msg = Message.obtain();
135 |
136 | msg.arg1 = message_id;
137 | msg.obj = message_content;
138 | try {
139 | assert messenger != null;
140 | messenger.send(msg);
141 | } catch (android.os.RemoteException | AssertionError e1) {
142 | Log.w(getClass().getName(), "Exception sending message", e1);
143 | }
144 | }
145 |
146 | /**
147 | * Try to prefetch the data of the link
148 | * Will return exactly the same link if the link does not exist
149 | * or if the prefetch failed.
150 | *
151 | * @param sharedLink partial link
152 | * @return new link with the prefetched data
153 | */
154 | private Link prefetchLink(Link sharedLink) {
155 | Link prefetchedLink = new Link(sharedLink);
156 | try {
157 | NetworkManager manager = NetworkUtils.getNetworkManager(sharedLink.getAccount());
158 |
159 | if (manager.isCompatibleShaarli() && manager.login()) {
160 | prefetchedLink = manager.prefetchLinkData(sharedLink);
161 | } else {
162 | mError = new Exception("Could not connect to shaarli. Possibles causes: unhandled shaarli, bad username or password");
163 | Log.e("ERROR", mError.getMessage());
164 | }
165 | } catch (IOException | NullPointerException e) {
166 | mError = e;
167 | Log.e("ERROR", mError.getMessage());
168 | }
169 | return prefetchedLink;
170 | }
171 |
172 | /**
173 | * Check if the given credentials are correct
174 | *
175 | * @param account The account with the credentials
176 | * @return NO_ERROR if nothing is wrong
177 | */
178 | private int checkShaarli(final ShaarliAccount account) {
179 | final NetworkManager manager = NetworkUtils.getNetworkManager(account);
180 | Log.i(LOGGER_NAME, "Checking Shaarli account " + account);
181 | try {
182 | if (!manager.isCompatibleShaarli()) {
183 | return TOKEN_ERROR;
184 | }
185 | if (!manager.login()) {
186 | return LOGIN_ERROR;
187 | }
188 | } catch (final IOException e) {
189 | Log.e(LOGGER_NAME, "Error checking Shaarli credentials: " + e.getMessage(), e);
190 | mError = e;
191 | return NETWORK_ERROR;
192 | }
193 | return NO_ERROR;
194 | }
195 |
196 | private void postLink(Link link) {
197 | boolean posted = true; // Assume it is shared
198 | try {
199 | // Connect the user to the site :
200 | NetworkManager manager = NetworkUtils.getNetworkManager(link.getAccount());
201 | if (manager.isCompatibleShaarli() && manager.login()) {
202 | manager.pushLink(link);
203 | } else {
204 | mError = new Exception("Could not connect to the shaarli. Possibles causes : unhandled shaarli, bad username or password");
205 | posted = false;
206 | }
207 | } catch (IOException | NullPointerException e) {
208 | mError = e;
209 | Log.e("ERROR", e.getMessage());
210 | posted = false;
211 | }
212 |
213 | if (!posted) {
214 | sendNotificationShareError(link);
215 | } else {
216 | mToastHandler.post(new DisplayToast(getString(R.string.add_success)));
217 | Log.i("SUCCESS", "Success while sharing link");
218 | }
219 | }
220 |
221 | /**
222 | * Display Toast in the main thread
223 | * Thanks: http://stackoverflow.com/a/3955826
224 | */
225 | private class DisplayToast implements Runnable {
226 | private final String mText;
227 |
228 | public DisplayToast(String text) {
229 | mText = text;
230 | }
231 |
232 | public void run() {
233 | Toast.makeText(mContext, mText, Toast.LENGTH_SHORT).show();
234 | }
235 | }
236 |
237 | /**
238 | * Retrieve the title of a page
239 | *
240 | * @param url the page to get the title
241 | * @return the title page, "" if there is an error
242 | */
243 | @NonNull
244 | private String[] getPageTitleAndDescription(String url) {
245 | return NetworkUtils.loadTitleAndDescription(url);
246 | }
247 |
248 | private void sendNotificationShareError(Link link) {
249 | NotificationCompat.Builder mBuilder =
250 | new NotificationCompat.Builder(this, CHANNEL_ID)
251 | .setSmallIcon(R.drawable.ic_launcher)
252 | .setContentTitle("Failed to share " + link.getTitle())
253 | .setContentText("Press to try again")
254 | .setAutoCancel(true)
255 | .setPriority(NotificationCompat.PRIORITY_LOW);
256 |
257 | // Creates an explicit intent To relaunch this service
258 | Intent resultIntent = new Intent(this, NetworkService.class);
259 |
260 | resultIntent.putExtra("action", NetworkService.INTENT_POST);
261 | resultIntent.putExtra("link", link);
262 |
263 | resultIntent.putExtra(Intent.EXTRA_TEXT, link.getUrl());
264 | resultIntent.putExtra(Intent.EXTRA_SUBJECT, link.getTitle());
265 |
266 | // The stack builder object will contain an artificial back stack for the
267 | // started Activity.
268 | // This ensures that navigating backward from the Activity leads out of
269 | // your application to the Home screen.
270 | TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
271 | // Adds the Intent that starts the Activity to the top of the stack
272 | stackBuilder.addNextIntent(resultIntent);
273 | PendingIntent resultPendingIntent = PendingIntent.getService(
274 | this,
275 | 0,
276 | resultIntent,
277 | PendingIntent.FLAG_IMMUTABLE);
278 |
279 | mBuilder.setContentIntent(resultPendingIntent);
280 | NotificationManager mNotificationManager =
281 | (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
282 | // mId allows you to update the notification later on.
283 | mNotificationManager.notify(link.getUrl().hashCode(), mBuilder.build());
284 | }
285 |
286 | private void createNotificationChannel() {
287 | // Create the NotificationChannel, but only on API 26+ because
288 | // the NotificationChannel class is new and not in the support library
289 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
290 | CharSequence name = getString(R.string.notification_channel_error_name);
291 | String description = getString(R.string.notification_channel_error_description);
292 | int importance = NotificationManager.IMPORTANCE_DEFAULT;
293 | NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
294 | channel.setDescription(description);
295 | // Register the channel with the system; you can't change the importance
296 | // or other notification behaviors after this
297 | NotificationManager notificationManager = getSystemService(NotificationManager.class);
298 | notificationManager.createNotificationChannel(channel);
299 | }
300 | }
301 | }
302 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/utils/UserPreferences.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier.utils;
2 |
3 | import android.app.Activity;
4 | import android.content.SharedPreferences;
5 |
6 | import com.dimtion.shaarlier.R;
7 |
8 | import static android.content.Context.MODE_PRIVATE;
9 |
10 | public class UserPreferences {
11 | private boolean privateShare;
12 | private boolean openDialog;
13 | private boolean autoTitle;
14 | private boolean autoDescription;
15 | private boolean tweet;
16 | private boolean toot;
17 |
18 | public static UserPreferences load(Activity a) {
19 | UserPreferences p = new UserPreferences();
20 | SharedPreferences pref = a.getSharedPreferences(a.getString(R.string.params), MODE_PRIVATE);
21 | p.privateShare = pref.getBoolean(a.getString(R.string.p_default_private), true);
22 | p.openDialog = pref.getBoolean(a.getString(R.string.p_show_share_dialog), true);
23 | p.autoTitle = pref.getBoolean(a.getString(R.string.p_auto_title), true);
24 | p.autoDescription = pref.getBoolean(a.getString(R.string.p_auto_description), false);
25 | p.tweet = pref.getBoolean(a.getString(R.string.p_shaarli2twitter), false);
26 | p.toot = pref.getBoolean(a.getString(R.string.p_shaarli2mastodon), false);
27 |
28 | return p;
29 | }
30 |
31 | public boolean isPrivateShare() {
32 | return privateShare;
33 | }
34 |
35 | public void setPrivateShare(boolean m_privateShare) {
36 | this.privateShare = m_privateShare;
37 | }
38 |
39 | public boolean isOpenDialog() {
40 | return openDialog;
41 | }
42 |
43 | public void setOpenDialog(boolean m_prefOpenDialog) {
44 | this.openDialog = m_prefOpenDialog;
45 | }
46 |
47 | public boolean isAutoTitle() {
48 | return autoTitle;
49 | }
50 |
51 | public void setAutoTitle(boolean m_autoTitle) {
52 | this.autoTitle = m_autoTitle;
53 | }
54 |
55 | public boolean isAutoDescription() {
56 | return autoDescription;
57 | }
58 |
59 | public void setAutoDescription(boolean m_autoDescription) {
60 | this.autoDescription = m_autoDescription;
61 | }
62 |
63 | public boolean isTweet() { return tweet; }
64 |
65 | public void setTweet(boolean tweet) {
66 | this.tweet = tweet;
67 | }
68 |
69 | public boolean isToot() {
70 | return toot;
71 | }
72 |
73 | public void setToot(boolean toot) {
74 | this.toot = toot;
75 | }
76 | }
77 |
78 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_send_white.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_action_accept.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-hdpi/ic_action_accept.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_action_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-hdpi/ic_action_new.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_send_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-hdpi/ic_send_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_action_accept.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-mdpi/ic_action_accept.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_action_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-mdpi/ic_action_new.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_send_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-mdpi/ic_send_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_action_accept.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-xhdpi/ic_action_accept.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_action_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-xhdpi/ic_action_new.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_send_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-xhdpi/ic_send_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_action_accept.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-xxhdpi/ic_action_accept.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_action_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-xxhdpi/ic_action_new.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_send_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-xxhdpi/ic_send_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shaarli_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/app/src/main/res/drawable/shaarli_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_accounts_management.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
15 |
16 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_add_account.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
18 |
19 |
26 |
27 |
33 |
34 |
41 |
42 |
49 |
50 |
56 |
57 |
63 |
64 |
67 |
68 |
69 |
75 |
76 |
82 |
83 |
86 |
87 |
96 |
97 |
105 |
106 |
114 |
115 |
121 |
122 |
128 |
129 |
132 |
133 |
139 |
140 |
146 |
147 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
18 |
19 |
28 |
29 |
30 |
38 |
39 |
45 |
46 |
54 |
55 |
63 |
64 |
71 |
72 |
79 |
80 |
86 |
87 |
94 |
95 |
102 |
103 |
110 |
111 |
112 |
120 |
121 |
127 |
128 |
129 |
130 |
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_share.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
21 |
22 |
31 |
32 |
35 |
36 |
45 |
46 |
59 |
60 |
61 |
64 |
65 |
78 |
79 |
91 |
92 |
93 |
99 |
100 |
106 |
107 |
113 |
114 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/tags_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_accounts_management.xml:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_add_account.xml:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_share.xml:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/values-fr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Shaarlier
4 | Réglages
5 | Une erreur s\'est produite lors du partage
6 | Contenu non géré
7 | Votre lien a bien été partagé
8 | Liens privés par défaut
9 | " Description"
10 | L\'url donnée ne semble pas fonctionner
11 | Vous n\'êtes pas connecté à internet
12 | Le pseudo ou le mot de passe est incorrect
13 | L\'url donné n\'est pas un Shaarli compatible
14 | L\'url donnée n\'est pas valide
15 | Logo de Shaarli
16 | Plus à venir…
17 | Privé
18 | Partager sur Shaarli
19 | Les paramètres sont corrects
20 | tags (séparés par des espaces)
21 | Tester puis enregistrer
22 | À propos
23 | Comportement
24 | Afficher une boite de dialogue lors du partage
25 | Ajouter à Shaarli
26 | Ajouter à Shaarli
27 | " Titre"
28 | Pseudo
29 | Mot de passe
30 | Url de votre Shaarli :
31 | Pour utiliser cette application vous avez besoin d\'un Shaarli, un clone minimaliste de delicious.
32 | Framasoft propose des instances gratuites.
33 | \n\nCette application est fièrement développée par dimtion, vous pouvez retrouver mes autres projets site web.
34 | Chargement du title…
35 | Erreur chargement titre
36 | Charger automatiquement le titre de la page
37 | shaarli.monserveur.fr
38 | Aller à mon Shaarli
39 | Partager un lien
40 | Version inconnue
41 | (vide pour une nouvelle note)
42 | Lien à partager :
43 | (laisser vide pour une nouvelle note)
44 | Titre :
45 | Description :
46 | Add
47 | Gérer les comptes
48 | Compte par défaut
49 | Nom du compte
50 | Il n\'y a aucun compte à afficher
51 | Valider
52 | Comptes
53 | Nouveau compte
54 | Êtes-vous sûr de vouloir supprimer ce compte ?
55 | Erreur lors du chargement des tags
56 | Désactiver la validation du certificat (non sécurisé)
57 | Ajouter un compte
58 | Vous pensez que c\'est un bug ?
59 | Voulez-vous signaler ce bug ?
60 | Charger automatiquement la description
61 | Afficher Shaarlier dans la liste des navigateurs web
62 | Authentification HTTP
63 | Aucune description n\'a été trouvé
64 | Une erreur inconnue s\'est produite
65 | Chargement…
66 | Effacer le compte
67 | "Version %1$s"
68 | Avancé
69 | Tweeter
70 | Toot
71 | Twitter via shaarli2twitter (obsolète)
72 | Mastodon via shaarli2mastodon (obsolète)
73 | Ouvrir Shaarli
74 | edition
75 | Rest API Secret
76 | Share error
77 | Channel dedicated to sharing errors
78 | Utiliser le mot de passe (obsolète)
79 | Url :
80 | Batman
81 |
--------------------------------------------------------------------------------
/app/src/main/res/values-sk/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Shaarlier
4 | Nastavenia
5 | Pri zdieľaní nastala chyba
6 | Nespracovaný obsah
7 | Váš odkaz bol zdieľaný
8 | Nové odkazy sú automaticky súkromné
9 | Popis
10 | Adresa odkazu nefunguje
11 | Žiadne internetové pripojenie
12 | Nesprávne používateľské meno a/alebo heslo
13 | Adresa odkazu nie je kompatibilná so Shaarli
14 | Adresa odkazu je neplatná
15 | Nepodarilo sa načítať značky
16 | Logo Shaarli
17 | Viac v pláne…
18 | Súkromné
19 | Nastavenia sú OK
20 | značky (oddelené medzerami)
21 | Vyskúšať a uložiť
22 | Správanie
23 | Pri zdielaní zobraziť dialógové okno
24 | Pridať do Shaarli
25 | Pridať do Shaarli
26 | Meno
27 | Heslo
28 | Základné prihlásenie cez HTTP
29 | Adresa vášho Shaarli:
30 |
31 | Zdielať odkaz
32 | O aplikácii
33 | Na použitie tejto aplikácie potrebujete nainštalovaný Shaarli.
34 | Framasoft poskytuje zdarma dostupnú inštaláciu.
35 | \n\nTúto aplikáciu hrdo vyvíja dimtion, nájdete ho na dimtion.fr.
36 |
37 | - http://
38 | - https://
39 |
40 | Načítava sa názov…
41 | Názov sa nepodarilo načítať
42 | Popis sa nepodarilo načítať
43 | Automaticky načítať názov stránky
44 | Batman
45 | https://shaarli.myserver.org
46 | Otvoriť moje Shaarli
47 | Zdielať odkaz
48 | Neznáma verzia
49 | (nevypĺňajte, ak chcete pridať poznámku)
50 | Zdielať odkaz:
51 | Url:
52 | (nevypĺňajte, ak chcete pridať poznámku)
53 | Nadpis:
54 | Nadpis
55 | Popis:
56 | Účty
57 |
58 | Pridať
59 | Overiť
60 | Žiadne účty
61 | Správca účtov
62 | Názov účtu
63 | Prednastavený účet
64 | Vymazať účet
65 | Nový účet
66 | Určite chcete vymazať tento účet?
67 | Zakázať overenie certifikátu (nezabezpečené)
68 | Vyskytla sa chyba
69 | Pridať účet
70 | Myslíte si, že ide o chybu?
71 | Chcete tento problém nahlásiť?
72 | Načítava sa popis…
73 | Automaticky načítať popis stránky
74 | Zobraziť Shaarlier v zozname webových prehliadačov
75 | "Verzia %1$s"
76 | Tweetnuť
77 | Tootnuť
78 | Použiť twitter (vyžaduje rozšírenie shaarli2twitter)
79 | Použiť mastodon (vyžaduje rozšírenie shaarli2mastodon)
80 | Rozšírené
81 | Otvoriť Shaarli
82 | upraviť
83 | Share error
84 | Channel dedicated to sharing errors
85 | Use username/password (deprecated)
86 |
87 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Shaarlier
4 | Settings
5 | An error occurred during sharing
6 | Unhandled content
7 | Your link has been shared
8 | New links are private by default
9 | Description
10 | The given url does not seems to work
11 | No internet connection
12 | Wrong credentials
13 | The given url is not a compatible Shaarli instance
14 | The url is wrong
15 | Could not load tags
16 | Shaarli logo
17 | More to come…
18 | Private
19 | The settings are fine
20 | tags (separated by spaces)
21 | Try and save
22 | Behaviour
23 | Show dialog box when sharing
24 | Add to Shaarli
25 | Add to Shaarli
26 | Username
27 | Password
28 | HTTP Basic Authentication
29 | Your Shaarli url:
30 |
31 | com.dimtion.shaarliposter.parms
32 | com.dimtion.shaarliposter.p_url_shaarli
33 | com.dimtion.shaarliposter.p_username
34 | com.dimtion.shaarliposter.p_password
35 | com.dimtion.shaarliposter.p_basic_username
36 | com.dimtion.shaarliposter.p_basic_password
37 | com.dimtion.shaarliposter.p_validated
38 | com.dimtion.shaarlier.p_default_private
39 | com.dimtion.shaarlier.p_show_share_dialog
40 | com.dimtion.shaarlier.p_protocol
41 | com.dimtion.shaarlier.p_user_url
42 | com.dimtion.shaarlier.auto_title
43 | com.dimtion.shaarlier.auto_description
44 | com.dimtion.shaarlier.p_version
45 | com.dimtion.shaarlier.p_default_account
46 | com.dimtion.shaarlier.saved_tags
47 |
48 | Share a link
49 | About
50 | To use this app your need a Shaarli instance.
51 | Framasoft provides free managed instances.
52 | \n\nThis app is proudly developed by dimtion, find me on my website.
53 |
54 | - http://
55 | - https://
56 |
57 | Loading title…
58 | Title could not be loaded
59 | Description could not be loaded
60 | Automatically load page title
61 | Batman
62 | https://shaarli.myserver.org
63 | Open my Shaarli
64 | https://shaarli.dimtion.fr
65 | zizou.xena@gmail.com
66 | Share a link
67 | Version unknown
68 | (empty for a new note)
69 | Link to share:
70 | Url:
71 | (leave empty for a new note)
72 | Title:
73 | Title
74 | Description:
75 | Accounts
76 |
77 | Add
78 | Validate
79 | There is no account to show
80 | Manage accounts
81 | Account name
82 | Default account
83 | Delete account
84 | New account
85 | Are you sure you want to delete this account?
86 | com.dimtion.shaarlier.database_key
87 | Disable certificate validation (unsecure)
88 | An unknown error has occurred
89 | Add account
90 | You think this is a bug?
91 | Would you like to report this issue?
92 | Loading description…
93 | Automatically load page description
94 | Show Shaarlier in the list of web browsers
95 | "Version %1$s"
96 | com.dimtion.shaarlier.shaarli2twitter
97 | com.dimtion.shaarlier.shaarli2mastodon
98 | Tweet
99 | Toot
100 | Use twitter via shaarli2twitter (deprecated)
101 | Use mastodon via shaarli2mastodon (deprecated)
102 | Advanced
103 | Open Shaarli
104 | editing
105 | Rest API Secret
106 |
107 | Share error
108 | Channel dedicated to sharing errors
109 | Use username/password (deprecated)
110 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | kotlin_version = '1.9.0-Beta'
6 | }
7 | repositories {
8 | mavenCentral()
9 | google()
10 | }
11 | dependencies {
12 | classpath 'com.android.tools.build:gradle:8.0.2'
13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 |
15 | // NOTE: Do not place your application dependencies here; they belong
16 | // in the individual module build.gradle files
17 | }
18 | }
19 |
20 | allprojects {
21 | repositories {
22 | mavenCentral()
23 | google()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 | # Specifies the JVM arguments used for the daemon process.
10 | # The setting is particularly useful for tweaking memory settings.
11 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
12 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
13 | # When configured, Gradle will run in incubating parallel mode.
14 | # This option should only be used with decoupled projects. More details, visit
15 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
16 | # org.gradle.parallel=true
17 | android.enableJetifier=true
18 | android.useAndroidX=true
19 | org.gradle.unsafe.configuration-cache=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimtion/Shaarlier/ffb379a1c90799c9ca5293ba3a52408044d493a2/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Mar 08 21:33:44 PDT 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------