8 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/dimtion/shaarlier/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
--------------------------------------------------------------------------------
/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_main.xml:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_add_account.xml:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | I know, a todo file is not always
2 |
3 | ## To do ASAP
4 | - Feature : Handle redirection (E.g : shaarli.fr)
5 | - Bug : autocomplete field for tags is hidden by keyboard when a description is too long (help needed!)
6 |
7 | ## Planned for future releases
8 | - Save link in a draft in case of an error
9 | - Nicer UI
10 |
11 | ## Q&A
12 | - Try app on as many apps as possible
13 | - Try more device screen sizes
14 | - Try in low network conditions
15 | - Try on slow devices
16 |
17 | ## Long term evolutions (if requested)
18 | - See online Shaarli links
19 | - Save link later
20 | - Try to stay KISS
21 |
22 | [Any idea ?](https://github.com/dimtion/Shaarlier/issues)
23 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 25
5 | buildToolsVersion '25.0.1'
6 |
7 | defaultConfig {
8 | applicationId "com.dimtion.shaarlier"
9 | minSdkVersion 15
10 | targetSdkVersion 25
11 | versionCode 22
12 | versionName "1.4.0-alpha"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled true
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 | }
21 |
22 | dependencies {
23 | compile fileTree(dir: 'libs', include: ['*.jar'])
24 | compile 'com.android.support:appcompat-v7:25.3.1'
25 | compile 'org.jsoup:jsoup:1.9.2'
26 | }
27 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_accounts_management.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
15 |
16 |
20 |
21 |
--------------------------------------------------------------------------------
/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 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/Shaarlier.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/HttpSchemeHandlerActivity.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
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 | public class HttpSchemeHandlerActivity extends Activity {
10 |
11 | @Override
12 | protected void onCreate(Bundle savedInstanceState) {
13 | super.onCreate(savedInstanceState);
14 |
15 | Uri data = getIntent().getData();
16 | if(data != null) {
17 | String url = data.toString();
18 |
19 | Intent addActivityIntent = new Intent(this, AddActivity.class);
20 | addActivityIntent.setAction(Intent.ACTION_SEND);
21 | addActivityIntent.setType("text/plain");
22 | addActivityIntent.putExtra(Intent.EXTRA_TEXT, url);
23 | startActivity(addActivityIntent);
24 | } else {
25 | Toast.makeText(getApplicationContext(), R.string.add_not_handle, Toast.LENGTH_SHORT).show();
26 | }
27 |
28 | finish();
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/Tag.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Shaarlier
2 |
3 | [](https://www.codacy.com/app/zizou-xena/Shaarlier)
4 |
5 | A simple app for sharing links on your Shaarli.
6 |
7 | ## Features
8 | - Publish links on your Shaarli
9 | - Automatically add a title and a description to your links (as Shaarli does)
10 | - Optional dialog box for editing title, description or tags
11 | - Multiple Shaarlis
12 |
13 | ## Planned features
14 | See [TODO file](https://github.com/dimtion/Shaarlier/blob/master/TODO.md) for a detailed roadmap.
15 |
16 | ## Downloads
17 | - From the [Play Store](https://play.google.com/store/apps/details?id=com.dimtion.shaarlier)
18 | - From the [release tab](https://github.com/dimtion/Shaarlier/releases)
19 | - From the [Aptoide Store](http://dimtion.store.aptoide.com/app/market/com.dimtion.shaarlier/8/8917968/Shaarlier)
20 | - From [F-Droid](https://f-droid.org/repository/browse/?fdfilter=shaarlier&fdid=com.dimtion.shaarlier)
21 |
22 | ## Why ?
23 | I made this app because the official one doesn't feels right to me, in some cases you just need to save a link for reading it later. So launching the browser for that only task is quite too long (you know, I like when things are fast and easy).
24 |
25 | ## Links
26 | - Main (and best) fork of the project : https://github.com/shaarli/Shaarli/
27 | - Thanks Jonathan Hedley for [jsoup](http://jsoup.org/) under [MIT Licence](http://jsoup.org/license)
28 | - My own personnal [shaarli](https://shaarli.dimtion.fr)
29 |
30 | --------
31 |
32 | Software under [GPLv3](https://www.gnu.org/licenses/gpl.html)
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/SpaceTokenizer.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
2 |
3 | import android.text.SpannableString;
4 | import android.text.Spanned;
5 | import android.text.TextUtils;
6 | import android.widget.MultiAutoCompleteTextView;
7 |
8 | /**
9 | * Created by dimtion on 22/02/2015.
10 | * Custom tokenizer, found on : http://stackoverflow.com/a/4596652/1582589 by vsm *
11 | */
12 |
13 |
14 | class SpaceTokenizer implements MultiAutoCompleteTextView.Tokenizer {
15 |
16 | public int findTokenStart(CharSequence text, int cursor) {
17 | int i = cursor;
18 |
19 | while (i > 0 && text.charAt(i - 1) != ' ') {
20 | i--;
21 | }
22 | while (i < cursor && text.charAt(i) == ' ') {
23 | i++;
24 | }
25 |
26 | return i;
27 | }
28 |
29 | public int findTokenEnd(CharSequence text, int cursor) {
30 | int i = cursor;
31 | int len = text.length();
32 |
33 | while (i < len) {
34 | if (text.charAt(i) == ' ') {
35 | return i;
36 | } else {
37 | i++;
38 | }
39 | }
40 |
41 | return len;
42 | }
43 |
44 | public CharSequence terminateToken(CharSequence text) {
45 | int i = text.length();
46 |
47 | while (i > 0 && text.charAt(i - 1) == ' ') {
48 | i--;
49 | }
50 |
51 | if (i > 0 && text.charAt(i - 1) == ' ') {
52 | return text;
53 | } else {
54 | if (text instanceof Spanned) {
55 | SpannableString sp = new SpannableString(text + " ");
56 | TextUtils.copySpansFrom((Spanned) text, 0, text.length(),
57 | Object.class, sp, 0);
58 | return sp;
59 | } else {
60 | return text + " ";
61 | }
62 | }
63 | }
64 | }
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/DebugHelper.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
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 |
9 | import java.text.DateFormat;
10 | import java.text.SimpleDateFormat;
11 | import java.util.Date;
12 | import java.util.TimeZone;
13 |
14 |
15 | /**
16 | * Created by dimtion on 16/05/2015.
17 | * A class to help debugging, should not be in production
18 | */
19 | class DebugHelper {
20 |
21 | public static void sendMailDev(Activity context, String subject, String content) {
22 | Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts(
23 | "mailto", context.getString(R.string.developer_mail), null));
24 | intent.putExtra(Intent.EXTRA_SUBJECT, subject);
25 | intent.putExtra(Intent.EXTRA_TEXT, content);
26 |
27 | context.startActivity(Intent.createChooser(intent, "Debug report..."));
28 | }
29 |
30 | public static String generateReport(Exception e, Activity activity, String extra) {
31 | String[] errorMessage = {e.getMessage(), e.toString()};
32 |
33 | return generateReport(errorMessage, activity, extra);
34 | }
35 |
36 | public static String generateReport(String[] errorMessage, Activity activity, String extra){
37 | String message = "Feel free to add a little message : \n\n";
38 |
39 | message += "-----BEGIN REPORT-----\n";
40 | message += "Report type: DEBUG \n";
41 | message += "Android version: " + " " + Build.VERSION.RELEASE + "\n";
42 | try {
43 | message += "App version: " + activity.getPackageManager()
44 | .getPackageInfo(activity.getPackageName(), 0).versionName + "\n";
45 | } catch (PackageManager.NameNotFoundException e1) {
46 | e1.printStackTrace();
47 | }
48 | message += "Activity: " + activity.toString();
49 |
50 | TimeZone tz = TimeZone.getTimeZone("UTC");
51 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");
52 | df.setTimeZone(tz);
53 |
54 | message += df.format(new Date()) + "\n\n";
55 |
56 | for (String m : errorMessage) {
57 | message += m + "\n\n";
58 | }
59 |
60 | message += "-----EXTRA-----\n" + extra + "\n";
61 |
62 | message += "-----END REPORT-----\n\n";
63 | message += "Thanks for the report, I'll try to answer as soon as possible !\n";
64 |
65 | return message;
66 | }
67 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/ShaarliAccount.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
2 |
3 | import java.io.Serializable;
4 |
5 | /**
6 | * Created by dimtion on 11/05/2015.
7 | * A Shaarli Account
8 | */
9 |
10 | public class ShaarliAccount implements Serializable {
11 | private long id;
12 | private String urlShaarli;
13 | private String username;
14 | private String password;
15 | private String basicAuthUsername;
16 | private String basicAuthPassword;
17 | private String shortName;
18 | private byte[] initialVector;
19 | private boolean validateCert;
20 |
21 | @Override
22 | public String toString() {
23 | if (this.shortName.equals(""))
24 | return username;
25 | return shortName;
26 | }
27 |
28 | public long getId() {
29 | return id;
30 | }
31 |
32 | public void setId(long id) {
33 | this.id = id;
34 | }
35 |
36 | public String getUrlShaarli() {
37 | return urlShaarli;
38 | }
39 |
40 | public void setUrlShaarli(String urlShaarli) {
41 | this.urlShaarli = urlShaarli;
42 | }
43 |
44 | public String getUsername() {
45 | return username;
46 | }
47 |
48 | public void setUsername(String username) {
49 | this.username = username;
50 | }
51 |
52 | public String getPassword() {
53 | return password;
54 | }
55 |
56 | public void setPassword(String password) {
57 | this.password = password;
58 | }
59 |
60 | public String getBasicAuthUsername() {
61 | return basicAuthUsername;
62 | }
63 |
64 | public void setBasicAuthUsername(String basicAuthUsername) {
65 | this.basicAuthUsername = basicAuthUsername;
66 | }
67 |
68 | public String getBasicAuthPassword() {
69 | return basicAuthPassword;
70 | }
71 |
72 | public void setBasicAuthPassword(String basicAuthPassword) {
73 | this.basicAuthPassword = basicAuthPassword;
74 | }
75 |
76 | public String getShortName() {
77 | return shortName;
78 | }
79 |
80 | public void setShortName(String shortName) {
81 | this.shortName = shortName;
82 | }
83 |
84 | public byte[] getInitialVector() {
85 | return initialVector;
86 | }
87 |
88 | public void setInitialVector(byte[] initialVector) {
89 | this.initialVector = initialVector;
90 | }
91 |
92 | public boolean isValidateCert() {
93 | return validateCert;
94 | }
95 |
96 | public void setValidateCert(boolean validateCert) {
97 | this.validateCert = validateCert;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/AccountsManagementActivity.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
2 |
3 | import android.content.Intent;
4 | import android.database.SQLException;
5 | import android.os.Bundle;
6 | import android.support.v7.app.AppCompatActivity;
7 | import android.util.Log;
8 | import android.view.Menu;
9 | import android.view.MenuItem;
10 | import android.view.View;
11 | import android.widget.AdapterView;
12 | import android.widget.ArrayAdapter;
13 | import android.widget.ListView;
14 |
15 | import java.util.List;
16 |
17 |
18 | public class AccountsManagementActivity extends AppCompatActivity {
19 |
20 | @Override
21 | protected void onCreate(Bundle savedInstanceState) {
22 | super.onCreate(savedInstanceState);
23 | setContentView(R.layout.activity_accounts_management);
24 | }
25 |
26 | @Override
27 | protected void onResume() {
28 | super.onResume();
29 | final ListView accountsListView = (ListView) findViewById(R.id.accountListView);
30 | AccountsSource accountsSource = new AccountsSource(getApplicationContext());
31 | try {
32 | accountsSource.rOpen();
33 | List accountsList = accountsSource.getAllAccounts();
34 | ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, accountsList);
35 |
36 | accountsListView.setAdapter(adapter);
37 |
38 | if (accountsList.isEmpty())
39 | findViewById(R.id.noAccountToShow).setVisibility(View.VISIBLE);
40 | else
41 | findViewById(R.id.noAccountToShow).setVisibility(View.GONE);
42 |
43 |
44 | } catch (SQLException e) {
45 | Log.e("DB_ERROR", e.toString());
46 | } finally {
47 | accountsSource.close();
48 | }
49 | accountsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
50 | @Override
51 | public void onItemClick(AdapterView> arg0, View arg1, int position, long arg3) {
52 | ShaarliAccount clickedAccount = (ShaarliAccount) accountsListView.getItemAtPosition(position);
53 |
54 | addOrEditAccount(clickedAccount);
55 | }
56 | });
57 | }
58 |
59 | private void addOrEditAccount(ShaarliAccount account) {
60 | Intent intent = new Intent(this, AddAccountActivity.class);
61 | if (account != null) {
62 | intent.putExtra("_id", account.getId());
63 | Log.w("EDIT ACCOUNT", account.getShortName());
64 | }
65 | startActivity(intent);
66 | }
67 | @Override
68 | public boolean onCreateOptionsMenu(Menu menu) {
69 | // Inflate the menu; this adds items to the action bar if it is present.
70 | getMenuInflater().inflate(R.menu.menu_accounts_management, menu);
71 | return true;
72 | }
73 |
74 | @Override
75 | public boolean onOptionsItemSelected(MenuItem item) {
76 | // Handle action bar item clicks here. The action bar will
77 | // automatically handle clicks on the Home/Up button, so long
78 | // as you specify a parent activity in AndroidManifest.xml.
79 | int id = item.getItemId();
80 |
81 | //noinspection SimplifiableIfStatement
82 | if (id == R.id.action_add) {
83 | addOrEditAccount(null);
84 | }
85 |
86 | return super.onOptionsItemSelected(item);
87 | }
88 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/TagsSource.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
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 java.util.ArrayList;
10 | import java.util.List;
11 |
12 | /**
13 | * Created by dimtion on 12/05/2015.
14 | * Interface between the table TAGS and the JAVA objects
15 | */
16 | class TagsSource {
17 | private final String[] allColumns = {MySQLiteHelper.TAGS_COLUMN_ID,
18 | MySQLiteHelper.TAGS_COLUMN_ID_ACCOUNT,
19 | MySQLiteHelper.TAGS_COLUMN_TAG};
20 | private final MySQLiteHelper dbHelper;
21 | private SQLiteDatabase db;
22 |
23 | public TagsSource(Context context) {
24 | dbHelper = new MySQLiteHelper(context);
25 | }
26 |
27 | public void rOpen() throws SQLException {
28 | db = dbHelper.getReadableDatabase();
29 | }
30 |
31 | public void wOpen() throws SQLException {
32 | db = dbHelper.getWritableDatabase();
33 | }
34 |
35 | public void close() {
36 | dbHelper.close();
37 | }
38 |
39 | public List getAllTags() {
40 | List tags = new ArrayList<>();
41 |
42 | Cursor cursor = db.query(MySQLiteHelper.TABLE_TAGS, allColumns, null, null, null, null, null);
43 | cursor.moveToFirst();
44 | while (!cursor.isAfterLast()) {
45 | Tag account = cursorToTag(cursor);
46 | tags.add(account);
47 | cursor.moveToNext();
48 | }
49 |
50 | cursor.close();
51 | return tags;
52 | }
53 |
54 | public Tag createTag(ShaarliAccount masterAccount, String value) {
55 | Tag tag = new Tag();
56 | tag.setMasterAccount(masterAccount);
57 | tag.setValue(value.trim());
58 |
59 | ContentValues values = new ContentValues();
60 | values.put(MySQLiteHelper.TAGS_COLUMN_ID_ACCOUNT, masterAccount.getId());
61 | values.put(MySQLiteHelper.TAGS_COLUMN_TAG, tag.getValue());
62 |
63 | // If existing, do nothing :
64 | String[] getTagArgs = {String.valueOf(tag.getMasterAccountId()), tag.getValue()};
65 |
66 | Cursor cursor = db.query(MySQLiteHelper.TABLE_TAGS, allColumns,
67 | MySQLiteHelper.TAGS_COLUMN_ID_ACCOUNT + " = ? AND " +
68 | MySQLiteHelper.TAGS_COLUMN_TAG + " = ?",
69 | getTagArgs, null, null, null);
70 | try {
71 | cursor.moveToFirst();
72 | if (cursor.isAfterLast()) {
73 | long insertId = db.insert(MySQLiteHelper.TABLE_TAGS, null, values);
74 | tag.setId(insertId);
75 | return tag;
76 | } else {
77 | tag = cursorToTag(cursor);
78 | }
79 | } catch (Exception e){
80 | tag = null;
81 | } finally {
82 | cursor.close();
83 | }
84 | return tag;
85 | }
86 |
87 | private Tag cursorToTag(Cursor cursor) { // If necessary (later), load the full account in the tag
88 | Tag tag = new Tag();
89 | tag.setId(cursor.getLong(0));
90 | tag.setMasterAccountId(cursor.getLong(1));
91 | tag.setValue(cursor.getString(2));
92 | return tag;
93 | }
94 |
95 | private void deleteAllTags() {
96 | db.delete(MySQLiteHelper.TABLE_TAGS, null, null);
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/EncryptionHelper.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
2 |
3 | import android.util.Base64;
4 |
5 | import java.io.UnsupportedEncodingException;
6 | import java.security.InvalidAlgorithmParameterException;
7 | import java.security.InvalidKeyException;
8 | import java.security.NoSuchAlgorithmException;
9 | import java.security.SecureRandom;
10 |
11 | import javax.crypto.BadPaddingException;
12 | import javax.crypto.Cipher;
13 | import javax.crypto.IllegalBlockSizeException;
14 | import javax.crypto.KeyGenerator;
15 | import javax.crypto.NoSuchPaddingException;
16 | import javax.crypto.SecretKey;
17 | import javax.crypto.spec.IvParameterSpec;
18 | import javax.crypto.spec.SecretKeySpec;
19 |
20 | /**
21 | * Created by dimtion on 13/05/2015.
22 | * A simple class to encrypt and decrypt simple data
23 | * (Probably needs review)
24 | */
25 | class EncryptionHelper {
26 | public final static int KEY_LENGTH = 256;
27 | public final static int IV_LENGTH = 16;
28 |
29 | public static SecretKey generateKey() throws NoSuchAlgorithmException {
30 |
31 | SecureRandom secureRandom = new SecureRandom();
32 | // Do *not* seed secureRandom! Automatically seeded from system entropy.
33 | KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
34 | keyGenerator.init(KEY_LENGTH, secureRandom);
35 | return keyGenerator.generateKey();
36 | }
37 |
38 | public static byte[] generateInitialVector() {
39 | SecureRandom random = new SecureRandom();
40 | return random.generateSeed(IV_LENGTH);
41 | }
42 |
43 | public static String secretKeyToString(SecretKey secretKey) {
44 | return Base64.encodeToString(secretKey.getEncoded(), Base64.DEFAULT);
45 | }
46 |
47 | public static SecretKey stringToSecretKey(String stringKey) {
48 | byte[] encodedKey = Base64.decode(stringKey, Base64.DEFAULT);
49 | return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
50 | }
51 |
52 | private static byte[] encryptDecrypt(int mode, byte[] clear, SecretKey key, byte[] initialVector)
53 | throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
54 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
55 | IvParameterSpec ivParameterSpec = new IvParameterSpec(initialVector);
56 |
57 | cipher.init(mode, key, ivParameterSpec);
58 | return cipher.doFinal(clear);
59 | }
60 |
61 | public static byte[] encrypt(byte[] clear, SecretKey key, byte[] initialVector)
62 | throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
63 | return encryptDecrypt(Cipher.ENCRYPT_MODE, clear, key, initialVector);
64 | }
65 |
66 | public static byte[] decrypt(byte[] encrypted, SecretKey key, byte[] initialVector)
67 | throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
68 | return encryptDecrypt(Cipher.DECRYPT_MODE, encrypted, key, initialVector);
69 | }
70 |
71 | public static byte[] stringToBase64(String clear) throws UnsupportedEncodingException {
72 | return Base64.encode(clear.getBytes(), Base64.DEFAULT);
73 | }
74 |
75 | public static String base64ToString(byte[] data) throws UnsupportedEncodingException {
76 | return new String(Base64.decode(data, Base64.DEFAULT));
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
73 |
76 |
77 |
82 |
85 |
86 |
87 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/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 |
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 |
27 | Pseudo :
28 | Mot de passe :
29 | Url de votre Shaarli :
30 | Pour utiliser cette application vous avez besoin d\'un Shaarli, un clone minimaliste de delicious.
31 | Cette application est fièrement développée par dimtion, vous pouvez me trouver sur GitHub (et d\'autres recoins d\'internet).
32 | Chargement du title…
33 | Erreur chargement titre
34 | Charger automatiquement le titre de la page
35 | shaarli.monserveur.fr
36 | Aller à mon Shaarli
37 | Partager un lien
38 | Version inconnue
39 | (vide pour une nouvelle note)
40 | Lien à partager :
41 | (laisser vide pour une nouvelle note)
42 | Titre :
43 | Description :
44 | Add
45 | Gérer les comptes
46 | Compte par défaut
47 | Nom du compte :
48 | Il n\'y a aucun compte à afficher
49 | Valider
50 | Comptes
51 | Nouveau compte
52 | Êtes-vous sûr de vouloir supprimer ce compte ?
53 | Erreur lors du chargement des tags
54 | Désactiver la validation du certificat (non sécurisé)
55 | Ajouter un compte
56 | Vous pensez que c\'est un bug ?
57 | Voulez-vous signaler ce bug ?
58 | Charger automatiquement la description
59 | Afficher Shaarlier dans la liste des navigateurs web
60 | Authentification HTTP
61 | Aucune description n\'a été trouvé
62 | Une erreur inconnue s\'est produite
63 | Chargement…
64 | Effacer le compte
65 | "Version %1$s"
66 | Avancé
67 | Tweeter
68 | Twitter (nécessite shaarli2twitter)
69 |
--------------------------------------------------------------------------------
/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 |
104 |
105 |
113 |
114 |
120 |
121 |
122 |
123 |
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/share_dialog.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
15 |
16 |
21 |
22 |
27 |
28 |
35 |
36 |
42 |
43 |
46 |
47 |
54 |
55 |
66 |
67 |
68 |
74 |
75 |
78 |
79 |
91 |
92 |
104 |
105 |
106 |
115 |
116 |
124 |
125 |
133 |
134 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/AutoCompleteWrapper.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
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.ArrayAdapter;
10 | import android.widget.MultiAutoCompleteTextView;
11 | import android.widget.Toast;
12 |
13 | import java.util.List;
14 |
15 | /**
16 | * Created by dimtion on 21/02/2015.
17 | * Inspired from : http://stackoverflow.com/a/5051180
18 | * and : http://www.claytical.com/blog/android-dynamic-autocompletion-using-google-places-api
19 | */
20 | class AutoCompleteWrapper {
21 |
22 | private final MultiAutoCompleteTextView a_textView;
23 | private final Context a_context;
24 | private final ArrayAdapter adapter;
25 |
26 | public AutoCompleteWrapper(final MultiAutoCompleteTextView textView, Context context) {
27 | this.a_textView = textView;
28 | this.a_context = context;
29 |
30 | this.a_textView.setTokenizer(new SpaceTokenizer());
31 |
32 | this.adapter = new ArrayAdapter<>(a_context, R.layout.tags_list);
33 | this.a_textView.setAdapter(this.adapter);
34 | this.a_textView.setThreshold(1);
35 | updateTagsView();
36 |
37 | AutoCompleteRetriever task = new AutoCompleteRetriever();
38 | task.execute();
39 | }
40 |
41 | private void updateTagsView() {
42 | try {
43 | TagsSource tagsSource = new TagsSource(a_context);
44 | tagsSource.rOpen();
45 | List tagList = tagsSource.getAllTags();
46 | tagsSource.close();
47 |
48 | this.adapter.clear();
49 | this.adapter.addAll(tagList);
50 | this.adapter.notifyDataSetChanged();
51 |
52 | this.a_textView.setAdapter(this.adapter);
53 |
54 | } catch (Exception e){
55 | sendReport(e);
56 | }
57 | }
58 |
59 | private class AutoCompleteRetriever extends AsyncTask {
60 | private Exception mError;
61 | @Override
62 | protected Boolean doInBackground(String... foo) {
63 | AccountsSource accountsSource = new AccountsSource(a_context);
64 | accountsSource.rOpen();
65 | List accounts = accountsSource.getAllAccounts();
66 |
67 | Boolean success = true;
68 | /* For the moment we keep all the tags, if later somebody wants to have the tags
69 | ** separated for each accounts, we will see
70 | */
71 | for (ShaarliAccount account : accounts) {
72 | // Download tags :
73 | NetworkManager manager = new NetworkManager(account);
74 | TagsSource tagsSource = new TagsSource(a_context);
75 | try {
76 | if(manager.retrieveLoginToken() && manager.login()) {
77 | String[] awesompleteTags = manager.retrieveTagsFromAwesomplete();
78 | String[] wsTags = manager.retrieveTagsFromWs(); // Keep for compatibility
79 | if (awesompleteTags == null && wsTags == null) {
80 | mError = manager.getLastError();
81 | success = false;
82 | } else {
83 | tagsSource.wOpen();
84 | if(awesompleteTags!= null) {
85 | for (String tagValue : awesompleteTags) {
86 | tagsSource.createTag(account, tagValue.trim());
87 | }
88 | }
89 | if(wsTags != null) {
90 | for (String tagValue : wsTags) {
91 | tagsSource.createTag(account, tagValue);
92 | }
93 | }
94 | tagsSource.close();
95 | }
96 | } else {
97 | mError = new Exception("Could not login");
98 | success = false;
99 | }
100 |
101 | } catch (Exception e) {
102 | mError = e;
103 | success = false;
104 | Log.e("ERROR", e.toString());
105 | } finally {
106 | tagsSource.close();
107 | }
108 | }
109 |
110 | accountsSource.close();
111 | return success;
112 | }
113 |
114 | // onPostExecute displays the results of the AsyncTask.
115 | @Override
116 | protected void onPostExecute(Boolean r) {
117 | if(!r) {
118 | String error = (mError != null) ? mError.getMessage() : "";
119 | Toast.makeText(a_context, a_context.getString(R.string.error_retrieving_tags) + " -- " + error, Toast.LENGTH_LONG).show();
120 | } else {
121 | updateTagsView();
122 | }
123 | }
124 | }
125 |
126 | private void sendReport(final Exception error) {
127 | final Activity activity = (Activity) a_context;
128 | AlertDialog.Builder builder = new AlertDialog.Builder(a_context);
129 |
130 | builder.setMessage("Would you like to report this issue ?").setTitle("REPORT - Shaarlier: add link");
131 |
132 |
133 | final String extra = ""; // "Url Shaarli: " + account.getUrlShaarli();
134 |
135 | builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
136 | public void onClick(DialogInterface dialog, int id) {
137 | DebugHelper.sendMailDev(activity, "REPORT - Shaarlier: load tags", DebugHelper.generateReport(error, activity, extra));
138 | }
139 | });
140 | builder.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
141 | public void onClick(DialogInterface dialog, int id) {
142 | // User cancelled the dialog
143 | }
144 | });
145 | AlertDialog dialog = builder.create();
146 | dialog.show();
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/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 have been shared
8 | New links are private by default
9 |
10 | The given url does not seems to work
11 | No internet connection
12 | The username or the password is wrong
13 | The given url is not a compatible Shaarli
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 | Username :
26 | Password :
27 | HTTP Basic Authentication
28 | Your Shaarli url :
29 |
30 | com.dimtion.shaarliposter.parms
31 | com.dimtion.shaarliposter.p_url_shaarli
32 | com.dimtion.shaarliposter.p_username
33 | com.dimtion.shaarliposter.p_password
34 | com.dimtion.shaarliposter.p_basic_username
35 | com.dimtion.shaarliposter.p_basic_password
36 | com.dimtion.shaarliposter.p_validated
37 | com.dimtion.shaarlier.p_default_private
38 | com.dimtion.shaarlier.p_show_share_dialog
39 | com.dimtion.shaarlier.p_protocol
40 | com.dimtion.shaarlier.p_user_url
41 | com.dimtion.shaarlier.auto_title
42 | com.dimtion.shaarlier.auto_description
43 | com.dimtion.shaarlier.p_version
44 | com.dimtion.shaarlier.p_default_account
45 | com.dimtion.shaarlier.saved_tags
46 |
47 | Share an Shaarli
48 | About
49 | To use this app your need a Shaarli, a minimalist delicious clone.
50 | This app is proudly developed by dimtion, find me on GitHub (and on other places on the Internet).
51 |
52 | http://
53 | https://
54 |
55 | Loading title…
56 | Title could not be loaded
57 | Description could not be loaded
58 | Automatically load page title
59 | Batman
60 | http://shaarli.myserver.org
61 | Open my Shaarli
62 | https://shaarli.dimtion.fr
63 | zizou.xena@gmail.com
64 | Share a link
65 | Version unknown
66 | (empty for a new note)
67 | Link to share :
68 | Url :
69 | (leave empty for a new note)
70 | Title :
71 |
72 | Description :
73 | Accounts
74 |
75 | Add
76 | Validate
77 | There is no account to show
78 | Manage accounts
79 | Account name :
80 | Default account
81 | Delete account
82 | New account
83 | Are you sure you want to delete this account ?
84 | com.dimtion.shaarlier.database_key
85 | Disable certificate validation (unsecured)
86 | An unknown error has occurred
87 | Add account
88 | You think this is a bug ?
89 | Would you like to report this issue ?
90 | Loading description…
91 | Automatically load page description
92 | Show Shaarlier in the list of web browsers
93 | "Version %1$s"
94 | com.dimtion.shaarlier.shaarli2twitter
95 | Tweet
96 | Use twitter (needs shaarli2twitter plugin)
97 | Advanced
98 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/MySQLiteHelper.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
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 java.security.NoSuchAlgorithmException;
11 |
12 | import javax.crypto.SecretKey;
13 |
14 | /**
15 | * Created by dimtion on 11/05/2015.
16 | * This class update the db scheme when necessary
17 | */
18 | class MySQLiteHelper extends SQLiteOpenHelper {
19 |
20 |
21 | // Table : accounts
22 | public static final String TABLE_ACCOUNTS = "accounts";
23 | public static final String ACCOUNTS_COLUMN_ID = "_id";
24 | public static final String ACCOUNTS_COLUMN_URL_SHAARLI = "url_shaarli";
25 | public static final String ACCOUNTS_COLUMN_USERNAME = "username";
26 | public static final String ACCOUNTS_COLUMN_PASSWORD_CYPHER = "password_cypher";
27 | public static final String ACCOUNTS_COLUMN_SHORT_NAME = "short_name";
28 | public static final String ACCOUNTS_COLUMN_IV = "initial_vector";
29 | public static final String ACCOUNTS_COLUMN_VALIDATE_CERT = "validate_cert";
30 | public static final String ACCOUNTS_COLUMN_BASIC_AUTH_USERNAME = "basic_auth_username";
31 | public static final String ACCOUNTS_COLUMN_BASIC_AUTH_PASSWORD_CYPHER = "basic_auth_password_cypher";
32 |
33 | private static final String CREATE_TABLE_ACCOUNTS = "create table "
34 | + TABLE_ACCOUNTS + " ("
35 | + ACCOUNTS_COLUMN_ID + " integer primary key autoincrement, "
36 | + ACCOUNTS_COLUMN_URL_SHAARLI + " text NOT NULL, "
37 | + ACCOUNTS_COLUMN_USERNAME + " text NOT NULL, "
38 | + ACCOUNTS_COLUMN_PASSWORD_CYPHER + " BLOB, "
39 | + ACCOUNTS_COLUMN_SHORT_NAME + " text DEFAULT '', "
40 | + ACCOUNTS_COLUMN_IV + " BLOB,"
41 | + ACCOUNTS_COLUMN_VALIDATE_CERT + " integer DEFAULT 1,"
42 | + ACCOUNTS_COLUMN_BASIC_AUTH_USERNAME + " text NOT NULL, "
43 | + ACCOUNTS_COLUMN_BASIC_AUTH_PASSWORD_CYPHER + " BLOB); ";
44 |
45 | // Table : tags
46 | public static final String TABLE_TAGS = "tags";
47 | public static final String TAGS_COLUMN_ID = "_id";
48 | public static final String TAGS_COLUMN_ID_ACCOUNT = "account_id";
49 | public static final String TAGS_COLUMN_TAG = "tag";
50 | private static final String CREATE_TABLE_TAGS = "create table "
51 | + TABLE_TAGS + " ("
52 | + TAGS_COLUMN_ID + " integer primary key autoincrement, "
53 | + TAGS_COLUMN_ID_ACCOUNT + " integer NOT NULL, "
54 | + TAGS_COLUMN_TAG + " text NOT NULL ) ;";
55 | private static final String DATABASE_NAME = "shaarlier.db";
56 | private static final int DATABASE_VERSION = 3;
57 |
58 | // Database updates
59 | private static final String[][] UPDATE_DB = { // TODO : check updates
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 | };
73 | private final Context mContext;
74 |
75 |
76 |
77 | public MySQLiteHelper(Context context) {
78 | super(context, DATABASE_NAME, null, DATABASE_VERSION);
79 | this.mContext = context;
80 | }
81 |
82 | @Override
83 | public void onCreate(SQLiteDatabase db) {
84 | db.execSQL(CREATE_TABLE_ACCOUNTS);
85 | db.execSQL(CREATE_TABLE_TAGS);
86 |
87 | // Create a secret key :
88 | String id = mContext.getString(R.string.params);
89 | SharedPreferences prefs = this.mContext.getSharedPreferences(id, Context.MODE_PRIVATE);
90 | SharedPreferences.Editor editor = prefs.edit();
91 | SecretKey key;
92 | try {
93 | key = EncryptionHelper.generateKey();
94 | String sKey = EncryptionHelper.secretKeyToString(key);
95 |
96 | editor.putString(mContext.getString(R.string.dbKey), sKey);
97 | editor.apply();
98 | } catch (NoSuchAlgorithmException e) {
99 | e.printStackTrace();
100 | key = null;
101 | Log.e("SHAARLIER", e.getMessage());
102 | }
103 |
104 | // In case of an update :
105 | String url = prefs.getString(mContext.getString(R.string.p_user_url), "");
106 | String usr = prefs.getString(mContext.getString(R.string.p_username), "");
107 | String pwd = prefs.getString(mContext.getString(R.string.p_password), "");
108 | String busr = prefs.getString(mContext.getString(R.string.p_basic_username), "");
109 | String bpwd = prefs.getString(mContext.getString(R.string.p_basic_password), "");
110 | int protocol = prefs.getInt(mContext.getString(R.string.p_protocol), 0);
111 | Boolean isValidated = prefs.getBoolean(mContext.getString(R.string.p_validated), false);
112 |
113 | String[] prot = {"http://", "https://"};
114 | try {
115 | if (isValidated) {
116 | ContentValues values = new ContentValues();
117 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_URL_SHAARLI, prot[protocol] + url);
118 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_USERNAME, usr);
119 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_USERNAME, busr);
120 |
121 | // Generate the iv :
122 | byte[] iv = EncryptionHelper.generateInitialVector();
123 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_IV, iv);
124 |
125 | byte[] encoded = EncryptionHelper.stringToBase64(pwd);
126 | byte[] password_cipher = EncryptionHelper.encrypt(encoded, key, iv);
127 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_PASSWORD_CYPHER, password_cipher);
128 |
129 | byte[] basic_encoded = EncryptionHelper.stringToBase64(bpwd);
130 | byte[] basic_password_cipher = EncryptionHelper.encrypt(basic_encoded, key, iv);
131 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_PASSWORD_CYPHER, basic_password_cipher);
132 |
133 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_SHORT_NAME, "Shaarli");
134 |
135 | db.insert(MySQLiteHelper.TABLE_ACCOUNTS, null, values);
136 | }
137 | } catch (Exception e) {
138 | Log.e("ERROR", e.getMessage());
139 | }
140 | }
141 |
142 | @Override
143 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
144 | Log.w(MySQLiteHelper.class.getName(),
145 | "Upgrading database from version " + oldVersion + " to "
146 | + newVersion + ", which will destroy all old data");
147 | for (int i = oldVersion - 1; i < newVersion - 1; i++) {
148 | for (String query :
149 | UPDATE_DB[i]) {
150 | db.execSQL(query);
151 | }
152 | }
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_add_account.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
18 |
19 |
26 |
27 |
32 |
33 |
39 |
40 |
46 |
47 |
53 |
54 |
60 |
61 |
66 |
67 |
73 |
74 |
79 |
80 |
83 |
84 |
89 |
90 |
96 |
97 |
104 |
105 |
106 |
115 |
116 |
123 |
124 |
133 |
134 |
141 |
142 |
148 |
149 |
155 |
156 |
159 |
160 |
166 |
167 |
173 |
174 |
182 |
183 |
184 |
185 |
186 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/AccountsSource.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
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 |
10 | import java.util.ArrayList;
11 | import java.util.List;
12 |
13 | import javax.crypto.SecretKey;
14 |
15 | /**
16 | * Created by dimtion on 11/05/2015.
17 | * API for managing accounts
18 | */
19 | class AccountsSource {
20 |
21 | private final String[] allColumns = {
22 | MySQLiteHelper.ACCOUNTS_COLUMN_ID,
23 | MySQLiteHelper.ACCOUNTS_COLUMN_URL_SHAARLI,
24 | MySQLiteHelper.ACCOUNTS_COLUMN_USERNAME,
25 | MySQLiteHelper.ACCOUNTS_COLUMN_PASSWORD_CYPHER,
26 | MySQLiteHelper.ACCOUNTS_COLUMN_SHORT_NAME,
27 | MySQLiteHelper.ACCOUNTS_COLUMN_IV,
28 | MySQLiteHelper.ACCOUNTS_COLUMN_VALIDATE_CERT,
29 | MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_USERNAME,
30 | MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_PASSWORD_CYPHER,
31 | };
32 | private final MySQLiteHelper dbHelper;
33 | private final Context mContext;
34 | private SQLiteDatabase db;
35 |
36 | public AccountsSource(Context context) {
37 | dbHelper = new MySQLiteHelper(context);
38 | this.mContext = context;
39 | }
40 |
41 | public void rOpen() throws SQLException {
42 | db = dbHelper.getReadableDatabase();
43 | }
44 |
45 | public void wOpen() throws SQLException {
46 | db = dbHelper.getWritableDatabase();
47 | }
48 |
49 | public void close() {
50 | dbHelper.close();
51 | }
52 |
53 | public ShaarliAccount createAccount(String urlShaarli, String username, String password, String basicAuthUsername, String basicAuthPassword, String shortName, boolean validateCert) throws Exception {
54 | ContentValues values = new ContentValues();
55 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_URL_SHAARLI, urlShaarli);
56 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_USERNAME, username);
57 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_USERNAME, basicAuthUsername);
58 |
59 | // Generate the iv :
60 | byte[] iv = EncryptionHelper.generateInitialVector();
61 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_IV, iv);
62 |
63 | byte[] password_cipher = encryptPassword(password, iv);
64 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_PASSWORD_CYPHER, password_cipher);
65 |
66 | byte[] basic_password_cipher = encryptPassword(basicAuthPassword, iv);
67 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_PASSWORD_CYPHER, basic_password_cipher);
68 |
69 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_SHORT_NAME, shortName);
70 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_VALIDATE_CERT, validateCert ? 1:0 ); // Convert bool to int
71 |
72 | long insertId = db.insert(MySQLiteHelper.TABLE_ACCOUNTS, null, values);
73 | return getShaarliAccountById(insertId);
74 | }
75 |
76 | public List getAllAccounts() {
77 | List accounts = new ArrayList<>();
78 |
79 | Cursor cursor = db.query(MySQLiteHelper.TABLE_ACCOUNTS, allColumns, null, null, null, null, null);
80 | cursor.moveToFirst();
81 | while (!cursor.isAfterLast()) {
82 | ShaarliAccount account = cursorToAccount(cursor);
83 | if (account != null)
84 | accounts.add(account);
85 | cursor.moveToNext();
86 | }
87 |
88 | cursor.close();
89 | return accounts;
90 | }
91 |
92 | private SecretKey getSecretKey() {
93 | String id = mContext.getString(R.string.params);
94 | SharedPreferences prefs = this.mContext.getSharedPreferences(id, Context.MODE_PRIVATE);
95 |
96 | String sKey = prefs.getString(this.mContext.getString(R.string.dbKey), "");
97 | return EncryptionHelper.stringToSecretKey(sKey);
98 | }
99 |
100 | private byte[] encryptPassword(String clearPassword, byte[] initialVector) throws Exception {
101 | SecretKey key = getSecretKey();
102 | byte[] encoded = EncryptionHelper.stringToBase64(clearPassword);
103 | return EncryptionHelper.encrypt(encoded, key, initialVector);
104 | }
105 |
106 | private String decryptPassword(byte[] cipherData, byte[] initialVector) throws Exception {
107 | SecretKey key = getSecretKey();
108 | byte[] encodedPassword = EncryptionHelper.decrypt(cipherData, key, initialVector);
109 | return EncryptionHelper.base64ToString(encodedPassword);
110 | }
111 |
112 | //
113 | // Returns null if the account doesn't exist
114 | //
115 | public ShaarliAccount getShaarliAccountById(long id) {
116 | rOpen();
117 | Cursor cursor = db.query(MySQLiteHelper.TABLE_ACCOUNTS, allColumns, MySQLiteHelper.ACCOUNTS_COLUMN_ID + " = " + id, null,
118 | null, null, null);
119 | cursor.moveToFirst();
120 |
121 | ShaarliAccount account = cursorToAccount(cursor);
122 | cursor.close();
123 | close();
124 |
125 | return account;
126 | }
127 |
128 | private ShaarliAccount cursorToAccount(Cursor cursor) {
129 | if (cursor.isAfterLast())
130 | return null;
131 |
132 | ShaarliAccount account = new ShaarliAccount();
133 | account.setId(cursor.getLong(0));
134 | account.setUrlShaarli(cursor.getString(1));
135 | account.setUsername(cursor.getString(2));
136 | account.setInitialVector(cursor.getBlob(5));
137 | account.setValidateCert(cursor.getInt(6) == 1); // Convert int to bool
138 | account.setBasicAuthUsername(cursor.getString(7));
139 |
140 | byte[] password_cypher = cursor.getBlob(3);
141 | String password;
142 | try {
143 | password = decryptPassword(password_cypher, account.getInitialVector());
144 | } catch (Exception e) {
145 | e.printStackTrace();
146 | return null;
147 | }
148 | account.setPassword(password);
149 |
150 | byte[] basic_password_cypher = cursor.getBlob(8);
151 | String basic_password;
152 | try {
153 | basic_password = decryptPassword(basic_password_cypher, account.getInitialVector());
154 | } catch (Exception e) {
155 | e.printStackTrace();
156 | basic_password = "";
157 | }
158 | account.setBasicAuthPassword(basic_password);
159 |
160 | account.setShortName(cursor.getString(4));
161 |
162 | return account;
163 | }
164 |
165 | public void deleteAccount(ShaarliAccount account) {
166 | db.delete(MySQLiteHelper.TABLE_ACCOUNTS, MySQLiteHelper.ACCOUNTS_COLUMN_ID + " = " + account.getId(), null);
167 | }
168 |
169 | public void editAccount(ShaarliAccount account) throws Exception {
170 | String QUERY_WHERE = MySQLiteHelper.ACCOUNTS_COLUMN_ID + " = " + account.getId();
171 | ContentValues values = new ContentValues();
172 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_URL_SHAARLI, account.getUrlShaarli());
173 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_USERNAME, account.getUsername());
174 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_USERNAME, account.getBasicAuthUsername());
175 |
176 | // Generate a new iv :
177 | account.setInitialVector(EncryptionHelper.generateInitialVector());
178 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_IV, account.getInitialVector());
179 |
180 | byte[] password_cipher = encryptPassword(account.getPassword(), account.getInitialVector());
181 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_PASSWORD_CYPHER, password_cipher);
182 |
183 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_SHORT_NAME, account.getShortName());
184 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_VALIDATE_CERT, account.isValidateCert() ? 1:0); // convert bool to int
185 | byte[] basic_password_cipher = encryptPassword(account.getBasicAuthPassword(), account.getInitialVector());
186 | values.put(MySQLiteHelper.ACCOUNTS_COLUMN_BASIC_AUTH_PASSWORD_CYPHER, basic_password_cipher);
187 |
188 | db.update(MySQLiteHelper.TABLE_ACCOUNTS, values, QUERY_WHERE, null);
189 | }
190 |
191 | public ShaarliAccount getDefaultAccount() throws Exception {
192 | SharedPreferences prefs = this.mContext.getSharedPreferences(this.mContext.getString(R.string.params), Context.MODE_PRIVATE);
193 | long defaultAccountId = prefs.getLong(this.mContext.getString(R.string.p_default_account), -1);
194 |
195 | ShaarliAccount defaultAccount = getShaarliAccountById(defaultAccountId);
196 | if (defaultAccount == null) {
197 | rOpen();
198 | Cursor cursor = db.query(MySQLiteHelper.TABLE_ACCOUNTS, allColumns, null, null, null, null, MySQLiteHelper.ACCOUNTS_COLUMN_ID, "1");
199 | cursor.moveToFirst();
200 | defaultAccount = cursorToAccount(cursor);
201 | cursor.close();
202 | close();
203 | }
204 | return defaultAccount;
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/app/app.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | generateDebugSources
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
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.os.Build;
10 | import android.os.Bundle;
11 | import android.support.v7.app.AppCompatActivity;
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.widget.Button;
18 | import android.widget.CheckBox;
19 | import android.widget.EditText;
20 | import android.widget.LinearLayout;
21 | import android.widget.TextView;
22 |
23 |
24 | public class MainActivity extends AppCompatActivity {
25 | private boolean m_isNoAccount;
26 |
27 | @Override
28 | protected void onCreate(Bundle savedInstanceState) {
29 | super.onCreate(savedInstanceState);
30 | setContentView(R.layout.activity_main);
31 |
32 | // Make links clickable :
33 | ((TextView) findViewById(R.id.about_details)).setMovementMethod(LinkMovementMethod.getInstance());
34 |
35 | loadSettings();
36 |
37 | // Load custom design :
38 | TextView textVersion = (TextView) findViewById(R.id.text_version);
39 | try {
40 | String versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
41 | textVersion.setText(String.format(getString(R.string.version), versionName));
42 |
43 | } catch (PackageManager.NameNotFoundException e) {
44 | textVersion.setText(getText(R.string.text_version));
45 | }
46 | }
47 |
48 | @Override
49 | public void onResume() {
50 | super.onResume();
51 | AccountsSource accountsSource = new AccountsSource(getApplicationContext());
52 | accountsSource.rOpen();
53 | m_isNoAccount = accountsSource.getAllAccounts().isEmpty();
54 |
55 | Button manageAccountsButton = (Button) findViewById(R.id.button_manage_accounts);
56 | if (m_isNoAccount) {
57 | manageAccountsButton.setText(R.string.add_account);
58 | } else {
59 | manageAccountsButton.setText(R.string.button_manage_accounts);
60 | }
61 | }
62 |
63 | @Override
64 | public void onPause() {
65 | super.onPause();
66 | saveSettings();
67 | }
68 |
69 | private void saveSettings() {
70 | // Get user inputs :
71 | boolean isPrivate = ((CheckBox) findViewById(R.id.default_private)).isChecked();
72 | boolean isShareDialog = ((CheckBox) findViewById(R.id.show_share_dialog)).isChecked();
73 | boolean isAutoTitle = ((CheckBox) findViewById(R.id.auto_load_title)).isChecked();
74 | boolean isAutoDescription = ((CheckBox) findViewById(R.id.auto_load_description)).isChecked();
75 | boolean isHandlingHttpScheme = ((CheckBox) findViewById(R.id.handle_http_scheme)).isChecked();
76 | boolean useShaarli2twitter = ((CheckBox) findViewById(R.id.handle_twitter_plugin)).isChecked();
77 |
78 | // Save data :
79 | SharedPreferences pref = getSharedPreferences(getString(R.string.params), MODE_PRIVATE);
80 | SharedPreferences.Editor editor = pref.edit();
81 | editor.putBoolean(getString(R.string.p_default_private), isPrivate)
82 | .putBoolean(getString(R.string.p_show_share_dialog), isShareDialog)
83 | .putBoolean(getString(R.string.p_auto_title), isAutoTitle)
84 | .putBoolean(getString(R.string.p_auto_description), isAutoDescription)
85 | .putBoolean(getString(R.string.p_shaarli2twitter), useShaarli2twitter)
86 | .apply();
87 |
88 | setHandleHttpScheme(isHandlingHttpScheme);
89 | }
90 |
91 | public void openAccountsManager(View view) {
92 | Intent intent;
93 | if (m_isNoAccount) {
94 | intent = new Intent(this, AddAccountActivity.class);
95 | } else {
96 | intent = new Intent(this, AccountsManagementActivity.class);
97 |
98 | }
99 | startActivity(intent);
100 |
101 | }
102 |
103 | private void loadSettings() {
104 | // Retrieve user previous settings
105 | SharedPreferences pref = getSharedPreferences(getString(R.string.params), MODE_PRIVATE);
106 | // updateSettingsFromUpdate(pref);
107 |
108 | boolean prv = pref.getBoolean(getString(R.string.p_default_private), false);
109 | boolean sherDial = pref.getBoolean(getString(R.string.p_show_share_dialog), true);
110 | boolean isAutoTitle = pref.getBoolean(getString(R.string.p_auto_title), true);
111 | boolean isAutoDescription = pref.getBoolean(getString(R.string.p_auto_description), false);
112 | boolean isHandlingHttpScheme = isHandlingHttpScheme();
113 | boolean isShaarli2twitter = pref.getBoolean(getString(R.string.p_shaarli2twitter), false);
114 |
115 | // Retrieve interface :
116 | CheckBox privateCheck = (CheckBox) findViewById(R.id.default_private);
117 | CheckBox shareDialogCheck = (CheckBox) findViewById(R.id.show_share_dialog);
118 | CheckBox autoTitleCheck = (CheckBox) findViewById(R.id.auto_load_title);
119 | CheckBox autoDescriptionCheck = (CheckBox) findViewById(R.id.auto_load_description);
120 | CheckBox handleHttpSchemeCheck = (CheckBox) findViewById(R.id.handle_http_scheme);
121 | CheckBox shaarli2twitter = (CheckBox) findViewById(R.id.handle_twitter_plugin);
122 |
123 | // Display user previous settings :
124 | privateCheck.setChecked(prv);
125 | autoTitleCheck.setChecked(isAutoTitle);
126 | autoDescriptionCheck.setChecked(isAutoDescription);
127 | handleHttpSchemeCheck.setChecked(isHandlingHttpScheme);
128 | shareDialogCheck.setChecked(sherDial);
129 | shaarli2twitter.setChecked(isShaarli2twitter);
130 | }
131 |
132 | @Override
133 | public boolean onCreateOptionsMenu(Menu menu) {
134 | // Inflate the menu; this adds items to the action bar if it is present.
135 | getMenuInflater().inflate(R.menu.menu_main, menu);
136 | if (m_isNoAccount) {
137 | menu.findItem(R.id.action_share).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
138 | } else {
139 | menu.findItem(R.id.action_share).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
140 | }
141 |
142 | return true;
143 | }
144 |
145 | @Override
146 | public boolean onOptionsItemSelected(MenuItem item) {
147 | // Handle action bar item clicks here. The action bar will
148 | // automatically handle clicks on the Home/Up button, so long
149 | // as you specify a parent activity in AndroidManifest.xml.
150 | int id = item.getItemId();
151 |
152 | //noinspection SimplifiableIfStatement
153 | switch (id) {
154 | case R.id.action_share:
155 | AlertDialog.Builder alert = new AlertDialog.Builder(this);
156 |
157 | alert.setTitle(getString(R.string.share));
158 |
159 | // TODO : move this to a xml file :
160 | final LinearLayout layout = new LinearLayout(this);
161 |
162 | final TextView textView = new TextView(this);
163 | if (Build.VERSION.SDK_INT < 23) {
164 | //noinspection deprecation
165 | textView.setTextAppearance(this, android.R.style.TextAppearance_Medium);
166 | } else {
167 | textView.setTextAppearance(android.R.style.TextAppearance_Medium);
168 | }
169 | textView.setText(getText(R.string.text_new_url));
170 |
171 | // Set an EditText view to get user input
172 | final EditText input = new EditText(this);
173 | input.setInputType(InputType.TYPE_TEXT_VARIATION_URI);
174 | input.setHint(getText(R.string.hint_new_url));
175 |
176 | layout.setOrientation(LinearLayout.VERTICAL);
177 | layout.setPadding(10, 10, 20, 20);
178 |
179 | layout.addView(textView);
180 | layout.addView(input);
181 | alert.setView(layout);
182 |
183 | alert.setPositiveButton(getString(android.R.string.yes), new DialogInterface.OnClickListener() {
184 | public void onClick(DialogInterface dialog, int whichButton) {
185 | String value = input.getText().toString();
186 | Intent intent = new Intent(getBaseContext(), AddActivity.class);
187 | intent.setAction(Intent.ACTION_SEND);
188 | intent.setType("text/plain");
189 | intent.putExtra(Intent.EXTRA_TEXT, value);
190 | startActivity(intent);
191 | }
192 | });
193 |
194 | alert.setNegativeButton(getString(android.R.string.no), new DialogInterface.OnClickListener() {
195 | public void onClick(DialogInterface dialog, int whichButton) {
196 | // Canceled.
197 | }
198 | });
199 |
200 | alert.show();
201 | break;
202 | default:
203 | return true;
204 | }
205 |
206 | return super.onOptionsItemSelected(item);
207 | }
208 |
209 | private boolean isHandlingHttpScheme() {
210 | return getPackageManager().getComponentEnabledSetting(getHttpSchemeHandlingComponent())
211 | == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
212 | }
213 |
214 | private void setHandleHttpScheme(boolean handleHttpScheme) {
215 | if(handleHttpScheme == isHandlingHttpScheme()) return;
216 |
217 | int flag = (handleHttpScheme ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
218 | : PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
219 |
220 | getPackageManager().setComponentEnabledSetting(
221 | getHttpSchemeHandlingComponent(), flag, PackageManager.DONT_KILL_APP);
222 | }
223 |
224 | private ComponentName getHttpSchemeHandlingComponent() {
225 | return new ComponentName(this, HttpSchemeHandlerActivity.class);
226 | }
227 |
228 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/NetworkService.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
2 |
3 | import android.app.IntentService;
4 | import android.app.NotificationManager;
5 | import android.app.PendingIntent;
6 | import android.content.Context;
7 | import android.content.Intent;
8 | import android.os.Handler;
9 | import android.os.Looper;
10 | import android.os.Message;
11 | import android.os.Messenger;
12 | import android.support.v4.app.NotificationCompat;
13 | import android.support.v4.app.TaskStackBuilder;
14 | import android.util.Log;
15 | import android.widget.Toast;
16 |
17 | import java.io.IOException;
18 |
19 | public class NetworkService extends IntentService {
20 | public static final String EXTRA_MESSENGER="com.dimtion.shaarlier.networkservice.EXTRA_MESSENGER";
21 | public static final int NO_ERROR = 0;
22 | public static final int NETWORK_ERROR = 1;
23 | public static final int TOKEN_ERROR = 2;
24 | public static final int LOGIN_ERROR = 3;
25 |
26 | public static final int RETRIEVE_TITLE_ID = 1;
27 |
28 | private String loadedTitle;
29 |
30 | private Context mContext;
31 | private Handler mToastHandler;
32 | private Exception mError;
33 | private ShaarliAccount mShaarliAccount;
34 | private String loadedDescription;
35 |
36 | public NetworkService() {
37 | super("NetworkService");
38 | }
39 |
40 | @Override
41 | public void onCreate(){
42 | super.onCreate();
43 | mContext = this;
44 | mToastHandler = new Handler(Looper.getMainLooper());
45 | }
46 |
47 | @Override
48 | protected void onHandleIntent(Intent intent) {
49 | // Load the messenger to communicate back to the activity
50 | Messenger messenger = (Messenger)intent.getExtras().get(EXTRA_MESSENGER);
51 | Message msg = Message.obtain();
52 | String action = intent.getStringExtra("action");
53 |
54 |
55 | switch (action) {
56 | case "checkShaarli":
57 | ShaarliAccount accountToTest = (ShaarliAccount) intent.getSerializableExtra("account");
58 |
59 | msg.arg1 = checkShaarli(accountToTest);
60 | if (msg.arg1 == NETWORK_ERROR) {
61 | msg.obj = mError;
62 | }
63 | // Send back messages to the calling activity
64 | try {
65 | assert messenger != null;
66 | messenger.send(msg);
67 | } catch (android.os.RemoteException | AssertionError e1) {
68 | Log.w(getClass().getName(), "Exception sending message", e1);
69 | }
70 | break;
71 | case "postLink":
72 | String sharedUrl = intent.getStringExtra("sharedUrl");
73 | String title = intent.getStringExtra("title");
74 | String description = intent.getStringExtra("description");
75 | String tags = intent.getStringExtra("tags");
76 | boolean isPrivate = intent.getBooleanExtra("privateShare", true);
77 | boolean tweet = intent.getBooleanExtra("tweet", true);
78 |
79 | if ("".equals(title) && this.loadedTitle != null) {
80 | title = this.loadedTitle;
81 | this.loadedTitle = null;
82 | }
83 | if ("".equals(description) && this.loadedDescription != null) {
84 | description = this.loadedDescription;
85 | this.loadedDescription = null;
86 | }
87 | long accountId = intent.getLongExtra("chosenAccountId", -1);
88 |
89 | try {
90 | AccountsSource acs = new AccountsSource(this);
91 | this.mShaarliAccount = (accountId != -1 ? acs.getShaarliAccountById(accountId) : acs.getDefaultAccount());
92 | } catch (Exception e) {
93 | e.printStackTrace();
94 | sendNotificationShareError(sharedUrl, title, description, tags, isPrivate, tweet);
95 | }
96 | postLink(sharedUrl, title, description, tags, isPrivate, tweet);
97 | stopSelf();
98 | break;
99 | case "retrieveTitleAndDescription":
100 | this.loadedTitle = "";
101 | this.loadedDescription = "";
102 |
103 | String url = intent.getStringExtra("url");
104 |
105 | boolean autoTitle = intent.getBooleanExtra("autoTitle", true);
106 | boolean autoDescription = intent.getBooleanExtra("autoDescription", false);
107 |
108 | String[] pageTitleAndDescription = getPageTitleAndDescription(url);
109 |
110 | if (autoTitle){
111 | this.loadedTitle = pageTitleAndDescription[0];
112 | }
113 | if (autoDescription){
114 | this.loadedDescription = pageTitleAndDescription[1];
115 | }
116 |
117 | msg.arg1 = RETRIEVE_TITLE_ID;
118 | msg.obj = pageTitleAndDescription;
119 | // Send back messages to the calling activity
120 | try {
121 | assert messenger != null;
122 | messenger.send(msg);
123 | } catch (android.os.RemoteException | AssertionError e1) {
124 | Log.w(getClass().getName(), "Exception sending message", e1);
125 | }
126 | break;
127 | default:
128 | // Do nothing
129 | break;
130 | }
131 | }
132 |
133 | /**
134 | * Display Toast in the main thread
135 | * Thanks : http://stackoverflow.com/a/3955826
136 | */
137 | private class DisplayToast implements Runnable{
138 | private final String mText;
139 |
140 | public DisplayToast(String text){
141 | mText = text;
142 | }
143 |
144 | public void run(){
145 | Toast.makeText(mContext, mText, Toast.LENGTH_SHORT).show();
146 | }
147 | }
148 |
149 | /**
150 | * Check if the given credentials are correct
151 | * @param account The account with the credentials
152 | * @return NO_ERROR if nothing is wrong
153 | */
154 | private int checkShaarli(ShaarliAccount account){
155 |
156 | NetworkManager manager = new NetworkManager(account);
157 | try {
158 | if (!manager.retrieveLoginToken()) {
159 | return TOKEN_ERROR;
160 | }
161 | if (!manager.login()) {
162 | return LOGIN_ERROR;
163 | }
164 | } catch (IOException e) {
165 | mError = e;
166 | return NETWORK_ERROR;
167 |
168 | }
169 | return NO_ERROR;
170 | }
171 |
172 | private void postLink(String sharedUrl, String title, String description, String tags, boolean privateShare, boolean tweet){
173 | boolean posted = true; // Assume it is shared
174 | try {
175 | // Connect the user to the site :
176 | NetworkManager manager = new NetworkManager(mShaarliAccount);
177 | manager.setTimeout(60000); // Long for slow networks
178 | if(manager.retrieveLoginToken() && manager.login()) {
179 | manager.postLink(sharedUrl, title, description, tags, privateShare, tweet);
180 | } else {
181 | mError = new Exception("Could not connect to the shaarli. Possibles causes : unhandled shaarli, bad username or password");
182 | posted = false;
183 | }
184 | } catch (IOException | NullPointerException e) {
185 | mError = e;
186 | Log.e("ERROR", e.getMessage());
187 | posted = false;
188 | }
189 |
190 | if (!posted) {
191 | sendNotificationShareError(sharedUrl, title, description, tags, privateShare, tweet);
192 | } else {
193 | mToastHandler.post(new DisplayToast(getString(R.string.add_success)));
194 | Log.i("SUCCESS", "Success while sharing link");
195 | }
196 | }
197 |
198 | /**
199 | * Retrieve the title of a page
200 | * @param url the page to get the title
201 | * @return the title page, "" if there is an error
202 | */
203 | private String[] getPageTitleAndDescription(String url){
204 | return NetworkManager.loadTitleAndDescription(url);
205 | }
206 |
207 | private void sendNotificationShareError(String sharedUrl, String title, String description, String tags, boolean privateShare, boolean tweet){
208 | NotificationCompat.Builder mBuilder =
209 | new NotificationCompat.Builder(this)
210 | .setSmallIcon(R.drawable.ic_launcher)
211 | .setContentTitle("Failed to share " + title)
212 | .setContentText("Press to try again")
213 | .setAutoCancel(true)
214 | .setPriority(NotificationCompat.PRIORITY_LOW);
215 |
216 | // Creates an explicit intent To relaunch this service
217 | Intent resultIntent = new Intent(this, NetworkService.class);
218 |
219 | resultIntent.putExtra("action", "postLink");
220 | resultIntent.putExtra("sharedUrl", sharedUrl);
221 | resultIntent.putExtra("title", title);
222 | resultIntent.putExtra("description", description);
223 | resultIntent.putExtra("tags", tags);
224 | resultIntent.putExtra("privateShare", privateShare);
225 | resultIntent.putExtra("tweet", tweet);
226 | resultIntent.putExtra("chosenAccountId", this.mShaarliAccount.getId());
227 |
228 | resultIntent.putExtra(Intent.EXTRA_TEXT, sharedUrl);
229 | resultIntent.putExtra(Intent.EXTRA_SUBJECT, title);
230 |
231 | // The stack builder object will contain an artificial back stack for the
232 | // started Activity.
233 | // This ensures that navigating backward from the Activity leads out of
234 | // your application to the Home screen.
235 | TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
236 | // Adds the Intent that starts the Activity to the top of the stack
237 | stackBuilder.addNextIntent(resultIntent);
238 | PendingIntent resultPendingIntent = PendingIntent.getService(this, 0, resultIntent, 0);
239 |
240 | mBuilder.setContentIntent(resultPendingIntent);
241 | NotificationManager mNotificationManager =
242 | (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
243 | // mId allows you to update the notification later on.
244 | mNotificationManager.notify(sharedUrl.hashCode(), mBuilder.build());
245 | }
246 | }
247 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/NetworkManager.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
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.Base64;
8 | import android.util.Log;
9 | import android.webkit.URLUtil;
10 |
11 | import org.json.JSONArray;
12 | import org.jsoup.Connection;
13 | import org.jsoup.Jsoup;
14 | import org.jsoup.nodes.Document;
15 | import org.jsoup.nodes.Element;
16 |
17 | import java.io.IOException;
18 | import java.net.URLEncoder;
19 | import java.util.Map;
20 |
21 | /**
22 | * Created by dimtion on 05/04/2015.
23 | * This class handle all the communications with Shaarli and other web services
24 | */
25 | class NetworkManager {
26 | private static final int LOAD_TITLE_MAX_BODY_SIZE = 50240;
27 | private static final int DEFAULT_TIME_OUT = 10000;
28 |
29 | private static final String[] DESCRIPTION_SELECTORS = {
30 | "meta[property=og:description]",
31 | "meta[name=description]",
32 | "meta[name=twitter:description]",
33 | };
34 |
35 | private final String mShaarliUrl;
36 | private final String mUsername;
37 | private final String mPassword;
38 | private final boolean mValidateCert;
39 | private final String mBasicAuth;
40 | private Integer mTimeout = DEFAULT_TIME_OUT;
41 |
42 | private Map mCookies;
43 | private String mToken;
44 |
45 | private String mDatePostLink;
46 | private String mSharedUrl;
47 | private Exception mLastError;
48 |
49 | public Exception getLastError() {
50 | return mLastError;
51 | }
52 |
53 | NetworkManager(ShaarliAccount account) {
54 | this.mShaarliUrl = account.getUrlShaarli();
55 | this.mUsername = account.getUsername();
56 | this.mPassword = account.getPassword();
57 | this.mValidateCert = account.isValidateCert();
58 |
59 | if (!"".equals(account.getBasicAuthUsername()) && !"".equals(account.getBasicAuthPassword())) {
60 | String login = account.getBasicAuthUsername() + ":" + account.getBasicAuthPassword();
61 | this.mBasicAuth = new String(Base64.encode(login.getBytes(), Base64.NO_WRAP));
62 | } else {
63 | this.mBasicAuth = "";
64 | }
65 | }
66 |
67 | /**
68 | * Helper method which create a new connection to Shaarli
69 | * @param url the url of the shaarli
70 | * @param isPost true if we create a POST request, false for a GET request
71 | * @return pre-made jsoupConnection
72 | */
73 | private Connection createShaarliConnection(String url, boolean isPost){
74 | Connection jsoupConnection = Jsoup.connect(url);
75 |
76 | Connection.Method connectionMethod = isPost ? Connection.Method.POST : Connection.Method.GET;
77 | if (!"".equals(this.mBasicAuth)) {
78 | jsoupConnection = jsoupConnection.header("Authorization", "Basic " + this.mBasicAuth);
79 | }
80 | if (this.mCookies != null){
81 | jsoupConnection = jsoupConnection.cookies(this.mCookies);
82 | }
83 |
84 | return jsoupConnection
85 | .validateTLSCertificates(this.mValidateCert)
86 | .timeout(this.mTimeout)
87 | .followRedirects(true)
88 | .method(connectionMethod);
89 | }
90 |
91 | /**
92 | * Check if a string is an url
93 | * TODO : unit test on this, I'm not quite sure it is perfect...
94 | */
95 | public static boolean isUrl(String url) {
96 | return URLUtil.isValidUrl(url) && !"http://".equals(url);
97 | }
98 |
99 | /**
100 | * Change something which is close to a url to something that is really one
101 | */
102 | public static String toUrl(String givenUrl) {
103 | String finalUrl = givenUrl;
104 | String protocol = "http://"; // Default value
105 | if ("".equals(givenUrl)) {
106 | return givenUrl; // Edge case, maybe need some discussion
107 | }
108 |
109 | if (!finalUrl.endsWith("/")) {
110 | finalUrl += '/';
111 | }
112 |
113 | if (!(finalUrl.startsWith("http://") || finalUrl.startsWith("https://"))) {
114 | finalUrl = protocol + finalUrl;
115 | }
116 |
117 | return finalUrl;
118 | }
119 |
120 | /**
121 | * Method to test the network connection
122 | * @return true if the device is connected to the network
123 | */
124 | public static boolean testNetwork(Activity parentActivity) {
125 | ConnectivityManager connMgr = (ConnectivityManager) parentActivity.getSystemService(Context.CONNECTIVITY_SERVICE);
126 | NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
127 | return (networkInfo != null && networkInfo.isConnected());
128 | }
129 |
130 | /**
131 | * Static method to load the title of a web page
132 | * @param url the url of the web page
133 | * @return "" if there is an error, the page title in other cases
134 | */
135 | public static String[] loadTitleAndDescription(String url) {
136 | String title = "";
137 | String description = "";
138 | Document pageResp;
139 | try {
140 | pageResp = Jsoup.connect(url)
141 | .maxBodySize(LOAD_TITLE_MAX_BODY_SIZE) // Hopefully we won't need more data
142 | .followRedirects(true)
143 | .execute()
144 | .parse();
145 | title = pageResp.title();
146 | } catch (Exception e) {
147 | // Just abandon the task if there is a problem
148 | Log.e("NetworkManager", e.toString());
149 | return new String[]{title, description};
150 | }
151 |
152 | // Many ways to get the description
153 | for (String selector : DESCRIPTION_SELECTORS) {
154 | try {
155 | description = pageResp.head().select(selector).first().attr("content");
156 | }
157 | catch (Exception e){
158 | Log.i("NetworkManager", e.toString());
159 | }
160 | if (!"".equals(description)){
161 | break;
162 | }
163 | }
164 | return new String[]{title, description};
165 | }
166 |
167 | /**
168 | * Set the default timeout
169 | * @param timeout : timeout in seconds
170 | */
171 | public void setTimeout(int timeout) {
172 | this.mTimeout = timeout;
173 | }
174 |
175 | /**
176 | * Check the if website is a compatible shaarli, by downloading a token
177 | */
178 | public boolean retrieveLoginToken() throws IOException {
179 | final String loginFormUrl = this.mShaarliUrl + "?do=login";
180 | try {
181 |
182 | Connection.Response loginFormPage = this.createShaarliConnection(loginFormUrl, false).execute();
183 | this.mCookies = loginFormPage.cookies();
184 | this.mToken = loginFormPage.parse().body().select("input[name=token]").first().attr("value");
185 |
186 | } catch (NullPointerException | IllegalArgumentException e) {
187 | return false;
188 | }
189 | return true;
190 | }
191 |
192 | /**
193 | * Method which retrieve the cookie saying that we are logged in
194 | */
195 | public boolean login() throws IOException {
196 | final String loginUrl = this.mShaarliUrl;
197 | try {
198 | Connection.Response loginPage = this.createShaarliConnection(loginUrl, true)
199 | .data("login", this.mUsername)
200 | .data("password", this.mPassword)
201 | .data("token", this.mToken)
202 | .data("returnurl", this.mShaarliUrl)
203 | .execute();
204 |
205 | this.mCookies = loginPage.cookies();
206 | loginPage.parse().body().select("a[href=?do=logout]").first()
207 | .attr("href"); // If this fails, you're not connected
208 |
209 | } catch (NullPointerException e) {
210 | return false;
211 | }
212 | return true;
213 | }
214 |
215 | /**
216 | * Method which retrieve a token for posting links
217 | * Update the cookie, the token and the date
218 | * Assume being logged in
219 | */
220 | private void retrievePostLinkToken(String encodedSharedLink) throws IOException {
221 | final String postFormUrl = this.mShaarliUrl + "?post=" + encodedSharedLink;
222 |
223 | Connection.Response postFormPage = this.createShaarliConnection(postFormUrl, false)
224 | .execute();
225 | final Element postFormBody = postFormPage.parse().body();
226 |
227 | // Update our situation :
228 | this.mToken = postFormBody.select("input[name=token]").first().attr("value");
229 | this.mDatePostLink = postFormBody.select("input[name=lf_linkdate]").first().attr("value"); // Date choosen by the server
230 | this.mSharedUrl = postFormBody.select("input[name=lf_url]").first().attr("value");
231 | }
232 |
233 | /**
234 | * Method which publishes a link to shaarli
235 | * Assume being logged in
236 | */
237 | public void postLink(String sharedUrl, String sharedTitle, String sharedDescription, String sharedTags, boolean privateShare, boolean tweet)
238 | throws IOException {
239 | String encodedShareUrl = URLEncoder.encode(sharedUrl, "UTF-8");
240 | retrievePostLinkToken(encodedShareUrl);
241 |
242 | if (isUrl(sharedUrl)) { // In case the url isn't really one, just post the one chosen by the server.
243 | this.mSharedUrl = sharedUrl;
244 | }
245 |
246 | final String postUrl = this.mShaarliUrl + "?post=" + encodedShareUrl;
247 |
248 | Connection postPageConn = this.createShaarliConnection(postUrl, true)
249 | .data("save_edit", "Save")
250 | .data("token", this.mToken)
251 | .data("lf_tags", sharedTags)
252 | .data("lf_linkdate", this.mDatePostLink)
253 | .data("lf_url", this.mSharedUrl)
254 | .data("lf_title", sharedTitle)
255 | .data("lf_description", sharedDescription);
256 | if (privateShare) postPageConn.data("lf_private", "on");
257 | if (tweet) postPageConn.data("tweet", "on");
258 | postPageConn.execute(); // Then we post
259 | }
260 |
261 | /**
262 | * Method which retrieve tags from the WS (old shaarli)
263 | * Assume being logged in
264 | */
265 | public String[] retrieveTagsFromWs() {
266 | final String requestUrl = this.mShaarliUrl + "?ws=tags&term=+";
267 | String[] predictionsArr = {};
268 | try {
269 | String json = this.createShaarliConnection(requestUrl, true)
270 | .ignoreContentType(true)
271 | .execute()
272 | .body();
273 |
274 | JSONArray ja = new JSONArray(json);
275 | predictionsArr = new String[ja.length()];
276 | for (int i = 0; i < ja.length(); i++) {
277 | // add each entry to our array
278 | predictionsArr[i] = ja.getString(i);
279 | }
280 |
281 | } catch (Exception e) {
282 | this.mLastError = e;
283 | return predictionsArr;
284 | }
285 | return predictionsArr;
286 | }
287 |
288 | /**
289 | * Method which retrieve tags from awesomplete (new shaarli)
290 | * Assume being logged in
291 | */
292 | public String[] retrieveTagsFromAwesomplete() {
293 | final String requestUrl = this.mShaarliUrl + "?post=";
294 | String[] tags = {};
295 | try {
296 | String tagsString = this.createShaarliConnection(requestUrl, false)
297 | .execute()
298 | .parse()
299 | .body()
300 | .select("input[name=lf_tags]")
301 | .first()
302 | .attr("data-list");
303 | tags = tagsString.split(", ");
304 |
305 | } catch (Exception e) {
306 | this.mLastError = e;
307 | }
308 |
309 | return tags;
310 | }
311 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/AddAccountActivity.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
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.support.v7.app.AppCompatActivity;
14 | import android.util.Log;
15 | import android.view.Menu;
16 | import android.view.MenuItem;
17 | import android.view.View;
18 | import android.view.inputmethod.InputMethodManager;
19 | import android.widget.Button;
20 | import android.widget.CheckBox;
21 | import android.widget.EditText;
22 | import android.widget.Switch;
23 | import android.widget.Toast;
24 |
25 | import java.util.List;
26 |
27 |
28 | public class AddAccountActivity extends AppCompatActivity {
29 |
30 | private String urlShaarli;
31 | private String username;
32 | private String password;
33 | private String basicAuthUsername;
34 | private String basicAuthPassword;
35 | private String shortName;
36 | private ShaarliAccount account;
37 | private Boolean isDefaultAccount;
38 | private Boolean isValidateCert;
39 |
40 | private Boolean isEditing = false;
41 |
42 |
43 | private class networkHandler extends Handler{
44 | private final Activity mParent;
45 |
46 | public networkHandler(Activity parent){
47 | this.mParent = parent;
48 | }
49 |
50 | /**
51 | * Handle the arrival of a message coming from the network service.
52 | * @param msg the message given by the service
53 | */
54 | @Override
55 | public void handleMessage(Message msg){
56 | findViewById(R.id.tryConfButton).setVisibility(View.VISIBLE);
57 | findViewById(R.id.tryingConfSpinner).setVisibility(View.GONE);
58 |
59 | // Show the returned error
60 | switch (msg.arg1) {
61 | case NetworkService.NO_ERROR:
62 | Toast.makeText(getApplicationContext(), R.string.success_test, Toast.LENGTH_LONG).show();
63 | saveAccount();
64 | finish();
65 | break;
66 | case NetworkService.NETWORK_ERROR:
67 | ((EditText) findViewById(R.id.urlShaarliView)).setError(getString(R.string.error_connecting));
68 | enableSendReport((Exception) msg.obj);
69 | break;
70 | case NetworkService.TOKEN_ERROR:
71 | ((EditText) findViewById(R.id.urlShaarliView)).setError(getString(R.string.error_parsing_token));
72 | enableSendReport(new Exception("TOKEN ERROR"));
73 | break;
74 | case NetworkService.LOGIN_ERROR:
75 | ((EditText) findViewById(R.id.usernameView)).setError(getString(R.string.error_login));
76 | ((EditText) findViewById(R.id.passwordView)).setError(getString(R.string.error_login));
77 | enableSendReport(new Exception("LOGIN ERROR"));
78 | break;
79 | default:
80 | ((EditText) findViewById(R.id.urlShaarliView)).setError(getString(R.string.error_unknown));
81 | Toast.makeText(getApplicationContext(), R.string.error_unknown, Toast.LENGTH_LONG).show();
82 | enableSendReport(new Exception("UNKNOWN ERROR"));
83 | break;
84 | }
85 | }
86 |
87 | private void enableSendReport(final Exception error) {
88 | Button reportButton = (Button) findViewById(R.id.sendReportButton);
89 | reportButton.setVisibility(View.VISIBLE);
90 | reportButton.setOnClickListener(new View.OnClickListener() {
91 | @Override
92 | public void onClick(View v) {
93 | AlertDialog.Builder builder = new AlertDialog.Builder(mParent);
94 |
95 | builder.setMessage(R.string.report_issue).setTitle("REPORT - Shaarlier");
96 |
97 | final String extra = "Url Shaarli: " + urlShaarli;
98 |
99 | builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
100 | public void onClick(DialogInterface dialog, int id) {
101 | DebugHelper.sendMailDev(mParent, "REPORT - Shaarlier", DebugHelper.generateReport(error, mParent, extra));
102 | }
103 | });
104 | builder.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
105 | public void onClick(DialogInterface dialog, int id) {
106 | // User cancelled the dialog
107 | }
108 | });
109 | AlertDialog dialog = builder.create();
110 | dialog.show();
111 | }
112 | });
113 |
114 | }
115 | }
116 |
117 | @Override
118 | protected void onCreate(Bundle savedInstanceState) {
119 | super.onCreate(savedInstanceState);
120 | setContentView(R.layout.activity_add_account);
121 |
122 | Intent intent = getIntent();
123 | long accountId = intent.getLongExtra("_id", -1);
124 |
125 | if (accountId != -1) {
126 | isEditing = true;
127 | AccountsSource accountsSource = new AccountsSource(getApplicationContext());
128 | try {
129 | account = accountsSource.getShaarliAccountById(accountId);
130 | } catch (Exception e) {
131 | account = null;
132 | }
133 | fillFields();
134 | } else {
135 |
136 | AccountsSource source = new AccountsSource(getApplicationContext());
137 | source.rOpen();
138 | List allAccounts = source.getAllAccounts();
139 | if (allAccounts.isEmpty()) { // If it is the first account created
140 | CheckBox defaultCheck = (CheckBox) findViewById(R.id.defaultAccountCheck);
141 | defaultCheck.setChecked(true);
142 | defaultCheck.setEnabled(false);
143 | }
144 | }
145 | }
146 |
147 |
148 | /**
149 | * Fill the fields with the selected account when editing a new account
150 | */
151 | private void fillFields() {
152 | // Get the user inputs :
153 | ((EditText) findViewById(R.id.urlShaarliView)).setText(account.getUrlShaarli());
154 | ((EditText) findViewById(R.id.usernameView)).setText(account.getUsername());
155 | ((EditText) findViewById(R.id.passwordView)).setText(account.getPassword());
156 | ((EditText) findViewById(R.id.shortNameView)).setText(account.getShortName());
157 |
158 | if (!"".equals(account.getBasicAuthUsername())) {
159 | ((EditText) findViewById(R.id.basicUsernameView)).setText(account.getBasicAuthUsername());
160 | ((EditText) findViewById(R.id.basicPasswordView)).setText(account.getBasicAuthPassword());
161 | ((Switch) findViewById(R.id.basicAuthSwitch)).setChecked(true);
162 | enableBasicAuth(findViewById(R.id.basicAuthSwitch));
163 | }
164 |
165 | // Is it the default account ?
166 | SharedPreferences prefs = getSharedPreferences(getString(R.string.params), MODE_PRIVATE);
167 | this.isDefaultAccount = (prefs.getLong(getString(R.string.p_default_account), -1) == account.getId());
168 | ((CheckBox) findViewById(R.id.defaultAccountCheck)).setChecked(this.isDefaultAccount);
169 |
170 | findViewById(R.id.deleteAccountButton).setVisibility(View.VISIBLE);
171 | }
172 |
173 | /**
174 | * Handle the action of deletion : show a confirmation dialog then delete (if wanted)
175 | * @param view : The view needed for handling interface actions
176 | */
177 | public void deleteAccountAction(View view) {
178 | // Show dialog to confirm deletion
179 | AlertDialog.Builder builder = new AlertDialog.Builder(this);
180 | builder.setMessage(getString(R.string.text_confirm_deletion_account));
181 | builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
182 | @Override
183 | public void onClick(DialogInterface dialog, int which) {
184 | deleteAccount();
185 | finish();
186 | }
187 | });
188 |
189 | builder.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
190 | public void onClick(DialogInterface dialog, int which) {
191 | // Just dismiss the dialog
192 | }
193 | });
194 | builder.show();
195 | }
196 |
197 | /**
198 | * Delete the selected account from the database
199 | */
200 | private void deleteAccount() {
201 | AccountsSource source = new AccountsSource(getApplicationContext());
202 | source.wOpen();
203 | source.deleteAccount(this.account);
204 | source.close();
205 | }
206 |
207 | @Override
208 | public boolean onCreateOptionsMenu(Menu menu) {
209 | // Inflate the menu; this adds items to the action bar if it is present.
210 | getMenuInflater().inflate(R.menu.menu_add_account, menu);
211 | return true;
212 | }
213 |
214 | @Override
215 | public boolean onOptionsItemSelected(MenuItem item) {
216 | // Handle action bar item clicks here. The action bar will
217 | // automatically handle clicks on the Home/Up button, so long
218 | // as you specify a parent activity in AndroidManifest.xml.
219 | int id = item.getItemId();
220 |
221 | //noinspection SimplifiableIfStatement
222 | if (id == R.id.action_validate) {
223 | tryAndSaveAction(findViewById(id));
224 | }
225 |
226 | return super.onOptionsItemSelected(item);
227 | }
228 |
229 | /**
230 | * Obviously hide the keyboard
231 | * From : http://stackoverflow.com/a/7696791/1582589
232 | */
233 | private void hideKeyboard() {
234 | // Check if no view has focus:
235 | View view = this.getCurrentFocus();
236 | if (view != null) {
237 | InputMethodManager inputManager = (InputMethodManager) this.getSystemService(Context.INPUT_METHOD_SERVICE);
238 | inputManager.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
239 | }
240 | }
241 |
242 | public void enableBasicAuth(View toggle) {
243 | boolean checked = ((Switch) toggle).isChecked();
244 | findViewById(R.id.basicUsernameView).setEnabled(checked);
245 | findViewById(R.id.basicPasswordView).setEnabled(checked);
246 | findViewById(R.id.basicUsernameTextView).setEnabled(checked);
247 | findViewById(R.id.basicPasswordTextView).setEnabled(checked);
248 | findViewById(R.id.basicUsernameTextView).setVisibility(checked ? View.VISIBLE : View.GONE);
249 | findViewById(R.id.basicPasswordTextView).setVisibility(checked ? View.VISIBLE : View.GONE);
250 | findViewById(R.id.basicUsernameView).setVisibility(checked ? View.VISIBLE : View.GONE);
251 | findViewById(R.id.basicPasswordView).setVisibility(checked ? View.VISIBLE : View.GONE);
252 | }
253 |
254 |
255 | /**
256 | * Action which handle the press on the try and save button
257 | * @param view : needed for binding with interface actions
258 | */
259 | public void tryAndSaveAction(View view) {
260 |
261 | hideKeyboard();
262 | findViewById(R.id.tryingConfSpinner).setVisibility(View.VISIBLE);
263 | findViewById(R.id.tryConfButton).setVisibility(View.GONE);
264 |
265 | // Get the user inputs :
266 | final String urlShaarliInput = ((EditText) findViewById(R.id.urlShaarliView)).getText().toString();
267 | this.username = ((EditText) findViewById(R.id.usernameView)).getText().toString();
268 | this.password = ((EditText) findViewById(R.id.passwordView)).getText().toString();
269 | if (((Switch)findViewById(R.id.basicAuthSwitch)).isChecked()) {
270 | this.basicAuthUsername = ((EditText) findViewById(R.id.basicUsernameView)).getText().toString();
271 | this.basicAuthPassword = ((EditText) findViewById(R.id.basicPasswordView)).getText().toString();
272 | }
273 | else {
274 | this.basicAuthUsername = "";
275 | this.basicAuthPassword = "";
276 | }
277 | this.shortName = ((EditText) findViewById(R.id.shortNameView)).getText().toString();
278 | this.isDefaultAccount = ((CheckBox) findViewById(R.id.defaultAccountCheck)).isChecked();
279 | this.isValidateCert = !((CheckBox) findViewById(R.id.disableCertValidation)).isChecked();
280 |
281 | this.urlShaarli = NetworkManager.toUrl(urlShaarliInput);
282 |
283 | ((EditText) findViewById(R.id.urlShaarliView)).setText(this.urlShaarli); // Update the view
284 |
285 | // Create a fake account :
286 | ShaarliAccount accountToTest = new ShaarliAccount();
287 | accountToTest.setUrlShaarli(this.urlShaarli);
288 | accountToTest.setUsername(this.username);
289 | accountToTest.setPassword(this.password);
290 | accountToTest.setValidateCert(this.isValidateCert);
291 | accountToTest.setBasicAuthUsername(this.basicAuthUsername);
292 | accountToTest.setBasicAuthPassword(this.basicAuthPassword);
293 |
294 | // Try the configuration :
295 | Intent i = new Intent(this, NetworkService.class);
296 | i.putExtra("action", "checkShaarli");
297 | i.putExtra("account", accountToTest);
298 | i.putExtra(NetworkService.EXTRA_MESSENGER, new Messenger(new networkHandler(this)));
299 | startService(i);
300 | }
301 |
302 | /**
303 | * Save a new account into the database,
304 | * should be called only if the account was verified.
305 | */
306 | private void saveAccount() {
307 | AccountsSource accountsSource = new AccountsSource(getApplicationContext());
308 | accountsSource.wOpen();
309 | try {
310 | if (isEditing) { // Only update the database
311 | account.setUrlShaarli(this.urlShaarli);
312 | account.setUsername(this.username);
313 | account.setPassword(this.password);
314 | account.setBasicAuthUsername(this.basicAuthUsername);
315 | account.setBasicAuthPassword(this.basicAuthPassword);
316 | account.setShortName(this.shortName);
317 | account.setValidateCert(this.isValidateCert);
318 | accountsSource.editAccount(account);
319 | } else {
320 | this.account = accountsSource.createAccount(this.urlShaarli, this.username, this.password, this.basicAuthUsername, this.basicAuthPassword, this.shortName, this.isValidateCert);
321 | }
322 | } catch (Exception e) {
323 | Log.e("ENCRYPTION ERROR", e.getMessage());
324 | } finally {
325 | accountsSource.close();
326 |
327 | }
328 |
329 | // Set the default account if needed
330 | if (this.isDefaultAccount) {
331 | SharedPreferences prefs = getSharedPreferences(getString(R.string.params), MODE_PRIVATE);
332 | SharedPreferences.Editor editor = prefs.edit();
333 |
334 | editor.putLong(getString(R.string.p_default_account), this.account.getId());
335 | editor.apply();
336 | }
337 | }
338 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dimtion/shaarlier/AddActivity.java:
--------------------------------------------------------------------------------
1 | package com.dimtion.shaarlier;
2 |
3 | import android.app.Activity;
4 | import android.app.AlertDialog;
5 | import android.content.DialogInterface;
6 | import android.content.Intent;
7 | import android.content.SharedPreferences;
8 | import android.os.Bundle;
9 | import android.os.Handler;
10 | import android.os.Message;
11 | import android.os.Messenger;
12 | import android.support.v4.app.ShareCompat;
13 | import android.text.Editable;
14 | import android.text.TextWatcher;
15 | import android.view.ContextThemeWrapper;
16 | import android.view.LayoutInflater;
17 | import android.view.View;
18 | import android.widget.ArrayAdapter;
19 | import android.widget.CheckBox;
20 | import android.widget.EditText;
21 | import android.widget.MultiAutoCompleteTextView;
22 | import android.widget.Spinner;
23 | import android.widget.Toast;
24 |
25 | import java.util.Collections;
26 | import java.util.List;
27 |
28 |
29 | public class AddActivity extends Activity {
30 | private ShaarliAccount chosenAccount;
31 | private List allAccounts;
32 | private Boolean privateShare;
33 | private boolean autoTitle;
34 | private boolean autoDescription;
35 | private boolean stopLoadingTitle;
36 | private boolean stopLoadingDescription;
37 | private boolean m_prefOpenDialog;
38 | private boolean tweet;
39 |
40 | private View a_dialogView;
41 |
42 | private class networkHandler extends Handler {
43 | private final Activity mParent;
44 |
45 | public networkHandler(Activity parent) {
46 | this.mParent = parent;
47 | }
48 |
49 | /**
50 | * Handle the arrival of a message coming from the network service.
51 | *
52 | * @param msg the message given by the service
53 | */
54 | @Override
55 | public void handleMessage(Message msg) {
56 | switch (msg.arg1) {
57 | case NetworkService.RETRIEVE_TITLE_ID:
58 | if (m_prefOpenDialog) {
59 | String title = ((String[]) msg.obj)[0];
60 | String description = ((String[]) msg.obj)[1];
61 |
62 | if(!stopLoadingTitle && autoTitle) {
63 | if ("".equals(title)) {
64 | updateTitle(title, true);
65 | } else {
66 | updateTitle(title, false);
67 | }
68 | }
69 | if(!stopLoadingDescription && autoDescription) {
70 | if ("".equals(description)) {
71 | updateDescription(description, true);
72 | } else {
73 | updateDescription(description, false);
74 | }
75 | }
76 | }
77 | break;
78 | default:
79 | Toast.makeText(getApplicationContext(), R.string.error_unknown, Toast.LENGTH_LONG).show();
80 | break;
81 | }
82 | }
83 | }
84 |
85 | @Override
86 | protected void onCreate(Bundle savedInstanceState) {
87 | super.onCreate(savedInstanceState);
88 | // setContentView(R.layout.activity_add);
89 | Intent intent = getIntent();
90 | String action = intent.getAction();
91 | String type = intent.getType();
92 |
93 | // Get the user preferences :
94 | SharedPreferences pref = getSharedPreferences(getString(R.string.params), MODE_PRIVATE);
95 | this.privateShare = pref.getBoolean(getString(R.string.p_default_private), true);
96 | this.m_prefOpenDialog = pref.getBoolean(getString(R.string.p_show_share_dialog), true);
97 | this.autoTitle = pref.getBoolean(getString(R.string.p_auto_title), true);
98 | this.autoDescription = pref.getBoolean(getString(R.string.p_auto_description), false);
99 | this.tweet = pref.getBoolean(getString(R.string.p_shaarli2twitter), false);
100 | stopLoadingTitle = false;
101 | stopLoadingDescription = false;
102 |
103 | // Check if there is at least one account, if so launch the settings :
104 | getAllAccounts();
105 | if (this.allAccounts.isEmpty()) {
106 | Intent intentLaunchSettings = new Intent(this, MainActivity.class);
107 | startActivity(intentLaunchSettings);
108 | } else if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) {
109 | ShareCompat.IntentReader reader = ShareCompat.IntentReader.from(this);
110 | String sharedUrl = reader.getText().toString();
111 |
112 | String sharedUrlTrimmed = this.extractUrl(sharedUrl);
113 | String defaultTitle = this.extractTitle(reader);
114 |
115 | String defaultDescription = intent.getStringExtra("description") != null ? intent.getStringExtra("description") : "";
116 | String defaultTags = intent.getStringExtra("tags") != null ? intent.getStringExtra("tags") : "";
117 |
118 | if (!autoTitle){
119 | defaultTitle = "";
120 | }
121 | if (!autoDescription){
122 | defaultDescription = "";
123 | }
124 |
125 | // Show edit dialog if the users wants
126 | if (m_prefOpenDialog) {
127 | handleDialog(sharedUrlTrimmed, defaultTitle, defaultDescription, defaultTags);
128 | } else {
129 | if (autoTitle || autoDescription) {
130 | loadAutoTitleAndDescription(sharedUrlTrimmed, defaultTitle, defaultDescription);
131 | }
132 | handleSendPost(sharedUrlTrimmed, defaultTitle, defaultDescription, defaultTags, privateShare, this.chosenAccount, this.tweet);
133 | }
134 | } else {
135 | Toast.makeText(getApplicationContext(), R.string.add_not_handle, Toast.LENGTH_SHORT).show();
136 | }
137 | }
138 |
139 | //
140 | // Default account first
141 | //
142 | private void getAllAccounts() {
143 | AccountsSource accountsSource = new AccountsSource(this);
144 | accountsSource.rOpen();
145 | this.allAccounts = accountsSource.getAllAccounts();
146 | try {
147 | this.chosenAccount = accountsSource.getDefaultAccount();
148 | } catch (Exception e) {
149 | e.printStackTrace();
150 | this.chosenAccount = null;
151 | } finally {
152 | accountsSource.close();
153 | }
154 |
155 | if (this.chosenAccount != null) {
156 | int indexChosenAccount = 0;
157 | for (ShaarliAccount account : this.allAccounts) {
158 | if (account.getId() == this.chosenAccount.getId()) {
159 | break;
160 | }
161 | indexChosenAccount++;
162 | }
163 | Collections.swap(this.allAccounts, indexChosenAccount, 0);
164 | }
165 | }
166 |
167 | //
168 | // Load the spinner for choosing the account
169 | //
170 | private void initAccountSpinner() {
171 | final Spinner accountSpinnerView = (Spinner) a_dialogView.findViewById(R.id.chooseAccount);
172 | ArrayAdapter adapter = new ArrayAdapter<>(this, R.layout.tags_list, this.allAccounts);
173 | accountSpinnerView.setAdapter(adapter);
174 | if (accountSpinnerView.getCount() <= 1) {
175 | accountSpinnerView.setVisibility(View.GONE);
176 | }
177 | }
178 |
179 | /**
180 | * Method to extract the url from shared data and delete trackers
181 | **/
182 | private String extractUrl(String sharedUrl) {
183 | String finalUrl;
184 | // trim the url because of annoying apps which send to much data :
185 | finalUrl = sharedUrl.trim();
186 |
187 | String[] possible_urls = finalUrl.split(" ");
188 |
189 | for (String url : possible_urls){
190 | if(NetworkManager.isUrl(url)){
191 | finalUrl = url;
192 | break;
193 | }
194 | }
195 |
196 | finalUrl = finalUrl.substring(finalUrl.lastIndexOf(" ") + 1);
197 | finalUrl = finalUrl.substring(finalUrl.lastIndexOf("\n") + 1);
198 |
199 | // If the url is incomplete :
200 | if (NetworkManager.isUrl("http://" + finalUrl) && !NetworkManager.isUrl(finalUrl)) {
201 | finalUrl = "http://" + finalUrl;
202 | }
203 | // Delete parameters added by trackers :
204 | if (finalUrl.contains("&utm_source=")) {
205 | finalUrl = finalUrl.substring(0, finalUrl.indexOf("&utm_source="));
206 | }
207 | if (finalUrl.contains("?utm_source=")) {
208 | finalUrl = finalUrl.substring(0, finalUrl.indexOf("?utm_source="));
209 | }
210 | if (finalUrl.contains("#xtor=RSS-")) {
211 | finalUrl = finalUrl.substring(0, finalUrl.indexOf("#xtor=RSS-"));
212 | }
213 |
214 | return finalUrl;
215 | }
216 |
217 | /**
218 | * Method to extract the title from shared data
219 | **/
220 | private String extractTitle(ShareCompat.IntentReader reader) {
221 | if (reader.getSubject() != null && !NetworkManager.isUrl(reader.getSubject())){
222 | return reader.getSubject();
223 | }
224 |
225 | return "";
226 | }
227 |
228 | //
229 | // Method made to handle the dialog box
230 | //
231 | private void handleDialog(final String sharedUrl, String givenTitle, String defaultDescription, String defaultTags) {
232 | AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.AppTheme));
233 | LayoutInflater inflater = AddActivity.this.getLayoutInflater();
234 | final View dialogView = inflater.inflate(R.layout.share_dialog, null);
235 | ((CheckBox) dialogView.findViewById(R.id.private_share)).setChecked(privateShare);
236 | this.a_dialogView = dialogView;
237 |
238 | // Init accountSpinner
239 | initAccountSpinner();
240 |
241 | // Load title or description:
242 | if ((autoDescription || autoTitle) && NetworkManager.isUrl(sharedUrl)) {
243 | loadAutoTitleAndDescription(sharedUrl, givenTitle, defaultDescription);
244 | }
245 |
246 | // Init url :
247 | ((EditText) dialogView.findViewById(R.id.url)).setText(sharedUrl);
248 |
249 | // Init tags :
250 | MultiAutoCompleteTextView textView = (MultiAutoCompleteTextView) dialogView.findViewById(R.id.tags);
251 | ((EditText) dialogView.findViewById(R.id.tags)).setText(defaultTags);
252 | new AutoCompleteWrapper(textView, this);
253 |
254 | // Init the tweet button if necessary:
255 | CheckBox tweetCheckBox = ((CheckBox) dialogView.findViewById(R.id.tweet));
256 | if (!this.tweet) {
257 | tweetCheckBox.setVisibility(View.GONE);
258 | tweetCheckBox.setChecked(false);
259 | } else {
260 | tweetCheckBox.setVisibility(View.VISIBLE);
261 | tweetCheckBox.setChecked(true);
262 | }
263 |
264 | // Open the dialog :
265 | builder.setView(dialogView)
266 | .setTitle(R.string.share)
267 | .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
268 | public void onClick(DialogInterface dialog, int which) {
269 | // Retrieve user data
270 | String url = ((EditText) dialogView.findViewById(R.id.url)).getText().toString();
271 | String title = ((EditText) dialogView.findViewById(R.id.title)).getText().toString();
272 | String description = ((EditText) dialogView.findViewById(R.id.description)).getText().toString();
273 | String tags = ((EditText) dialogView.findViewById(R.id.tags)).getText().toString();
274 | privateShare = ((CheckBox) dialogView.findViewById(R.id.private_share)).isChecked();
275 | tweet = ((CheckBox) dialogView.findViewById(R.id.tweet)).isChecked();
276 | chosenAccount = (ShaarliAccount) ((Spinner) dialogView.findViewById(R.id.chooseAccount)).getSelectedItem();
277 |
278 | // Finally send everything
279 | handleSendPost(url, title, description, tags, privateShare, chosenAccount, tweet);
280 | }
281 | })
282 | .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
283 | public void onClick(DialogInterface dialog, int which) {
284 | finish();
285 | }
286 | }).setOnCancelListener(new DialogInterface.OnCancelListener() {
287 | @Override
288 | public void onCancel(DialogInterface dialog) {
289 | finish();
290 | }
291 | })
292 | .show();
293 | }
294 |
295 | // To get an automatic title :
296 | private void loadAutoTitleAndDescription(String sharedUrl, String defaultTitle, String defaultDescription) {
297 |
298 | // Don't use network ressources if not needed
299 | if (!autoTitle && !autoDescription){
300 | return;
301 | }
302 | // Launch intent to retrieve the title and the description
303 | final Intent networkIntent = new Intent(this, NetworkService.class);
304 | networkIntent.putExtra("action", "retrieveTitleAndDescription");
305 | networkIntent.putExtra("url", sharedUrl);
306 | networkIntent.putExtra("autoTitle", autoTitle);
307 | networkIntent.putExtra("autoDescription", autoDescription);
308 | networkIntent.putExtra(NetworkService.EXTRA_MESSENGER, new Messenger(new networkHandler(this)));
309 |
310 | stopLoadingTitle = false;
311 | stopLoadingDescription = false;
312 | startService(networkIntent);
313 |
314 | // Everything is done in the NetworkService if no dialog is opened
315 | if (!m_prefOpenDialog){
316 | return;
317 | }
318 |
319 | if (autoDescription) {
320 | if ("".equals(defaultDescription)) {
321 | a_dialogView.findViewById(R.id.loading_description).setVisibility(View.VISIBLE);
322 | ((EditText) a_dialogView.findViewById(R.id.description)).setHint(R.string.loading_description_hint);
323 |
324 | // If in the meanwhile the user type text in the field, stop retrieving the description.
325 | ((EditText) a_dialogView.findViewById(R.id.description)).addTextChangedListener(new TextWatcher() {
326 | @Override
327 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {
328 | stopLoadingDescription = true;
329 | a_dialogView.findViewById(R.id.loading_description).setVisibility(View.GONE);
330 | ((EditText) a_dialogView.findViewById(R.id.description)).removeTextChangedListener(this);
331 | }
332 |
333 | @Override
334 | public void onTextChanged(CharSequence s, int start, int before, int count) {
335 | // Nothing to be done
336 | }
337 |
338 | @Override
339 | public void afterTextChanged(Editable s) {
340 | // Nothing to be done
341 | }
342 | });
343 | } else {
344 | stopLoadingDescription = true;
345 | updateDescription(defaultDescription, false);
346 | }
347 | }
348 |
349 | if (autoTitle) {
350 | if ("".equals(defaultTitle)) {
351 | a_dialogView.findViewById(R.id.loading_title).setVisibility(View.VISIBLE);
352 | ((EditText) a_dialogView.findViewById(R.id.title)).setHint(R.string.loading_title_hint);
353 | // If in the meanwhile the user type text in the field, stop retrieving the title.
354 | ((EditText) a_dialogView.findViewById(R.id.title)).addTextChangedListener(new TextWatcher() {
355 | @Override
356 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {
357 | stopLoadingTitle = true;
358 | a_dialogView.findViewById(R.id.loading_title).setVisibility(View.GONE);
359 | ((EditText) a_dialogView.findViewById(R.id.title)).removeTextChangedListener(this);
360 | }
361 |
362 | @Override
363 | public void onTextChanged(CharSequence s, int start, int before, int count) {
364 | // Nothing to be done
365 | }
366 |
367 | @Override
368 | public void afterTextChanged(Editable s) {
369 | // Nothing to be done
370 | }
371 | });
372 | } else {
373 | stopLoadingTitle = true;
374 | updateTitle(defaultTitle, false);
375 | }
376 | }
377 | }
378 |
379 | private void updateTitle(String title, boolean isError) {
380 | ((EditText) a_dialogView.findViewById(R.id.title)).setHint(R.string.title_hint);
381 |
382 | if (isError) {
383 | ((EditText) a_dialogView.findViewById(R.id.title)).setHint(R.string.error_retrieving_title);
384 | } else {
385 | ((EditText) a_dialogView.findViewById(R.id.title)).setText(title);
386 | }
387 |
388 | a_dialogView.findViewById(R.id.loading_title).setVisibility(View.GONE);
389 | }
390 |
391 | private void updateDescription(String description, boolean isError){
392 | ((EditText) a_dialogView.findViewById(R.id.description)).setHint(R.string.description_hint);
393 |
394 | if (isError) {
395 | ((EditText) a_dialogView.findViewById(R.id.description)).setHint(R.string.error_retrieving_description);
396 | } else {
397 | ((EditText) a_dialogView.findViewById(R.id.description)).setText(description);
398 | }
399 |
400 | a_dialogView.findViewById(R.id.loading_description).setVisibility(View.GONE);
401 | }
402 |
403 | /**
404 | * Start the network service to send the link with all its data to Shaarli
405 | * @param sharedUrl the one url
406 | * @param title a chosen title by the user
407 | * @param description user description
408 | * @param tags user tags
409 | * @param isPrivate true if the link is private
410 | * @param account the account which the share operate
411 | */
412 | private void handleSendPost(String sharedUrl, String title, String description, String tags, boolean isPrivate, ShaarliAccount account, boolean tweet){
413 | Intent networkIntent = new Intent(this, NetworkService.class);
414 | networkIntent.putExtra("action", "postLink");
415 | networkIntent.putExtra("sharedUrl", sharedUrl);
416 | networkIntent.putExtra("title", title);
417 | networkIntent.putExtra("description", description);
418 | networkIntent.putExtra("tags", tags);
419 | networkIntent.putExtra("privateShare", isPrivate);
420 | networkIntent.putExtra("tweet", tweet);
421 | networkIntent.putExtra("chosenAccountId", account.getId());
422 | networkIntent.putExtra(NetworkService.EXTRA_MESSENGER, new Messenger(new networkHandler(this)));
423 |
424 | startService(networkIntent);
425 | finish();
426 | }
427 | }
428 |
429 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
--------------------------------------------------------------------------------