diff --git a/app/build.gradle b/app/build.gradle index 5ab820972..989e687b8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,9 +7,8 @@ android { applicationId "fr.gouv.etalab.mastodon" minSdkVersion 15 targetSdkVersion 25 - versionCode 5 - versionName "1.0.5" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + versionCode 7 + versionName "1.1.0" } buildTypes { release { @@ -21,17 +20,11 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { - exclude group: 'com.android.support', module: 'support-annotations' - }) compile 'com.android.support:appcompat-v7:25.3.1' compile 'com.android.support:design:25.3.1' compile 'com.android.support:support-v4:25.3.1' - compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4' compile 'com.loopj.android:android-async-http:1.4.9' compile 'com.google.code.gson:gson:2.8.0' - compile 'com.squareup.retrofit2:retrofit:2.2.0' compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' - compile 'com.evernote:android-job:1.1.9' - testCompile 'junit:junit:4.12' + compile 'com.evernote:android-job:1.1.10' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8cc184ac3..a6b2329f3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,10 +42,6 @@ android:configChanges="keyboardHidden|orientation|screenSize" android:theme="@style/AppTheme.NoActionBar"> - . */ package fr.gouv.etalab.mastodon.activities; +import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.graphics.Color; -import android.os.Build; +import android.net.Uri; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; -import android.text.Html; -import android.text.method.LinkMovementMethod; import android.view.MenuItem; +import android.view.View; +import android.widget.Button; import android.widget.TextView; import mastodon.etalab.gouv.fr.mastodon.R; @@ -48,24 +48,34 @@ public class AboutActivity extends AppCompatActivity { about_version.setText(getResources().getString(R.string.about_vesrion, version)); } catch (PackageManager.NameNotFoundException ignored) {} - TextView about_developer = (TextView) findViewById(R.id.about_developer); - TextView about_license = (TextView) findViewById(R.id.about_license); - TextView about_code = (TextView) findViewById(R.id.about_code); - about_developer.setMovementMethod(LinkMovementMethod.getInstance()); - about_license.setMovementMethod(LinkMovementMethod.getInstance()); - about_code.setMovementMethod(LinkMovementMethod.getInstance()); - about_developer.setLinkTextColor(Color.BLUE); - about_license.setLinkTextColor(Color.BLUE); - about_code.setLinkTextColor(Color.BLUE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - about_developer.setText(Html.fromHtml(getString(R.string.about_developer), Html.FROM_HTML_MODE_COMPACT)); - about_license.setText(Html.fromHtml(getString(R.string.about_license), Html.FROM_HTML_MODE_COMPACT)); - about_code.setText(Html.fromHtml(getString(R.string.about_code), Html.FROM_HTML_MODE_COMPACT)); - }else { - about_developer.setText(Html.fromHtml(getString(R.string.about_developer))); - about_license.setText(Html.fromHtml(getString(R.string.about_license))); - about_code.setText(Html.fromHtml(getString(R.string.about_code))); - } + Button about_developer = (Button) findViewById(R.id.about_developer); + Button about_code = (Button) findViewById(R.id.about_code); + Button about_license = (Button) findViewById(R.id.about_license); + + about_code.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://bitbucket.org/tom79/mastodon_etalab")); + startActivity(browserIntent); + } + }); + about_developer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(AboutActivity.this, ShowAccountActivity.class); + Bundle b = new Bundle(); + b.putString("accountId", "2416"); + intent.putExtras(b); + startActivity(intent); + } + }); + about_license.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.gnu.org/licenses/quick-guide-gplv3.fr.html")); + startActivity(browserIntent); + } + }); } diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/activities/LoginActivity.java b/app/src/main/java/fr/gouv/etalab/mastodon/activities/LoginActivity.java index 77e82b560..30715b650 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/activities/LoginActivity.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/activities/LoginActivity.java @@ -15,26 +15,37 @@ package fr.gouv.etalab.mastodon.activities; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; +import android.os.AsyncTask; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Button; +import android.widget.EditText; import android.widget.Toast; +import com.loopj.android.http.AsyncHttpClient; import com.loopj.android.http.AsyncHttpResponseHandler; +import com.loopj.android.http.RequestParams; import org.json.JSONException; import org.json.JSONObject; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; import java.util.HashMap; import cz.msebera.android.httpclient.Header; +import fr.gouv.etalab.mastodon.asynctasks.UpdateAccountInfoAsyncTask; +import fr.gouv.etalab.mastodon.client.MastalabSSLSocketFactory; import fr.gouv.etalab.mastodon.client.OauthClient; import fr.gouv.etalab.mastodon.helper.Helper; import mastodon.etalab.gouv.fr.mastodon.R; +import static fr.gouv.etalab.mastodon.helper.Helper.USER_AGENT; + /** * Created by Thomas on 23/04/2017. @@ -49,14 +60,23 @@ public class LoginActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); - final Button connectionButton = (Button) findViewById(R.id.login_button); - final Intent webviewIntent = new Intent(this, WebviewActivity.class); - - + Button connectionButton = (Button) findViewById(R.id.login_button); connectionButton.setEnabled(false); + } + + @Override + protected void onResume(){ + super.onResume(); + Button connectionButton = (Button) findViewById(R.id.login_button); + if( !connectionButton.isEnabled()) + retrievesClientId(); + } + + private void retrievesClientId(){ + final Button connectionButton = (Button) findViewById(R.id.login_button); String action = "/api/v1/apps"; - HashMap parameters = new HashMap<>(); + final HashMap parameters = new HashMap<>(); parameters.put(Helper.CLIENT_NAME, Helper.OAUTH_REDIRECT_HOST); parameters.put(Helper.REDIRECT_URIS,"https://" + Helper.INSTANCE + Helper.REDIRECT_CONTENT); parameters.put(Helper.SCOPES, Helper.OAUTH_SCOPES); @@ -95,14 +115,54 @@ public class LoginActivity extends AppCompatActivity { connectionButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - startActivity(webviewIntent); - finish(); + connectionButton.setEnabled(false); + AsyncHttpClient client = new AsyncHttpClient(); + RequestParams requestParams = new RequestParams(); + SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); + requestParams.add(Helper.CLIENT_ID, sharedpreferences.getString(Helper.CLIENT_ID, null)); + requestParams.add(Helper.CLIENT_SECRET, sharedpreferences.getString(Helper.CLIENT_SECRET, null)); + requestParams.add("grant_type", "password"); + EditText login_uid = (EditText) findViewById(R.id.login_uid); + EditText login_passwd = (EditText) findViewById(R.id.login_passwd); + requestParams.add("username",login_uid.getText().toString().trim()); + requestParams.add("password",login_passwd.getText().toString().trim()); + client.setUserAgent(USER_AGENT); + try { + client.setSSLSocketFactory(new MastalabSSLSocketFactory(MastalabSSLSocketFactory.getKeystore())); + client.post("https://" + Helper.INSTANCE + "/oauth/token", requestParams, new AsyncHttpResponseHandler() { + @Override + public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) { + String response = new String(responseBody); + JSONObject resobj; + try { + resobj = new JSONObject(response); + String token = resobj.get("access_token").toString(); + SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(Helper.PREF_KEY_OAUTH_TOKEN, token); + editor.apply(); + //Update the account with the token; + new UpdateAccountInfoAsyncTask(LoginActivity.this, token).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + @Override + public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) { + connectionButton.setEnabled(true); + Toast.makeText(getApplicationContext(),R.string.toast_error_login,Toast.LENGTH_LONG).show(); + } + }); + + } catch (NoSuchAlgorithmException | KeyManagementException | UnrecoverableKeyException | KeyStoreException e) { + e.printStackTrace(); + } + } }); - } - } diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/activities/MainActivity.java b/app/src/main/java/fr/gouv/etalab/mastodon/activities/MainActivity.java index 3a79eee0a..fee1fc9cc 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/activities/MainActivity.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/activities/MainActivity.java @@ -50,6 +50,7 @@ import java.util.HashMap; import fr.gouv.etalab.mastodon.asynctasks.UpdateAccountInfoByIDAsyncTask; import fr.gouv.etalab.mastodon.client.Entities.Account; +import fr.gouv.etalab.mastodon.client.PatchBaseImageDownloader; import fr.gouv.etalab.mastodon.fragments.DisplayAccountsFragment; import fr.gouv.etalab.mastodon.fragments.DisplayNotificationsFragment; import fr.gouv.etalab.mastodon.helper.Helper; @@ -62,8 +63,9 @@ import fr.gouv.etalab.mastodon.fragments.TabLayoutSettingsFragment; import fr.gouv.etalab.mastodon.sqlite.AccountDAO; import mastodon.etalab.gouv.fr.mastodon.R; +import static fr.gouv.etalab.mastodon.helper.Helper.HOME_TIMELINE_INTENT; import static fr.gouv.etalab.mastodon.helper.Helper.INTENT_ACTION; -import static fr.gouv.etalab.mastodon.helper.Helper.INTENT_NOTIFICATION; +import static fr.gouv.etalab.mastodon.helper.Helper.NOTIFICATION_INTENT; public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, OnUpdateAccountInfoInterface { @@ -119,6 +121,7 @@ public class MainActivity extends AppCompatActivity File cacheDir = new File(getCacheDir(), getString(R.string.app_name)); ImageLoaderConfiguration configImg = new ImageLoaderConfiguration.Builder(this) .threadPoolSize(5) + .imageDownloader(new PatchBaseImageDownloader(getApplicationContext())) .threadPriority(Thread.MIN_PRIORITY + 3) .denyCacheImageMultipleSizesInMemory() .diskCache(new UnlimitedDiskCache(cacheDir)) @@ -138,10 +141,14 @@ public class MainActivity extends AppCompatActivity boolean menuWasSelected = false; if( getIntent() != null && getIntent().getExtras() != null ){ Bundle extras = getIntent().getExtras(); - if (extras.getInt(INTENT_ACTION) == INTENT_NOTIFICATION){ + if (extras.getInt(INTENT_ACTION) == NOTIFICATION_INTENT){ navigationView.setCheckedItem(R.id.nav_notification); navigationView.getMenu().performIdentifierAction(R.id.nav_notification, 0); menuWasSelected = true; + }else if( extras.getInt(INTENT_ACTION) == HOME_TIMELINE_INTENT){ + navigationView.setCheckedItem(R.id.nav_home); + navigationView.getMenu().performIdentifierAction(R.id.nav_home, 0); + menuWasSelected = true; } } if (savedInstanceState == null && !menuWasSelected) { @@ -190,10 +197,13 @@ public class MainActivity extends AppCompatActivity return; Bundle extras = intent.getExtras(); if( extras.containsKey(INTENT_ACTION) ){ - if (extras.getInt(INTENT_ACTION) == INTENT_NOTIFICATION){ - final NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); + final NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); + if (extras.getInt(INTENT_ACTION) == NOTIFICATION_INTENT){ navigationView.setCheckedItem(R.id.nav_notification); navigationView.getMenu().performIdentifierAction(R.id.nav_notification, 0); + }else if( extras.getInt(INTENT_ACTION) == HOME_TIMELINE_INTENT){ + navigationView.setCheckedItem(R.id.nav_home); + navigationView.getMenu().performIdentifierAction(R.id.nav_home, 0); } } intent.replaceExtras(new Bundle()); diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/activities/MainApplication.java b/app/src/main/java/fr/gouv/etalab/mastodon/activities/MainApplication.java index 3ed2bfe00..f35014176 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/activities/MainApplication.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/activities/MainApplication.java @@ -18,10 +18,12 @@ import android.app.Application; import com.evernote.android.job.JobManager; import fr.gouv.etalab.mastodon.jobs.ApplicationJob; +import fr.gouv.etalab.mastodon.jobs.HomeTimelineSyncJob; import fr.gouv.etalab.mastodon.jobs.NotificationsSyncJob; /** * Created by Thomas on 29/04/2017. + * Main application, jobs are launched here. */ public class MainApplication extends Application{ @@ -32,6 +34,7 @@ public class MainApplication extends Application{ super.onCreate(); JobManager.create(this).addJobCreator(new ApplicationJob()); JobManager.instance().getConfig().setVerbose(false); - NotificationsSyncJob.schedule(getApplicationContext(), false); + NotificationsSyncJob.schedule(false); + HomeTimelineSyncJob.schedule(false); } } diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/activities/ShowAccountActivity.java b/app/src/main/java/fr/gouv/etalab/mastodon/activities/ShowAccountActivity.java index bd3998e29..ceacccd0b 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/activities/ShowAccountActivity.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/activities/ShowAccountActivity.java @@ -14,21 +14,28 @@ * see . */ package fr.gouv.etalab.mastodon.activities; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.content.LocalBroadcastManager; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.support.v7.app.AppCompatActivity; +import android.text.Html; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; @@ -77,8 +84,9 @@ public class ShowAccountActivity extends AppCompatActivity implements OnPostActi private ViewPager mPager; private String accountId; private TabLayout tabLayout; - - + private BroadcastReceiver hide_header; + private TextView account_note; + private String userId; public enum action{ FOLLOW, @@ -94,6 +102,9 @@ public class ShowAccountActivity extends AppCompatActivity implements OnPostActi super.onCreate(savedInstanceState); setContentView(R.layout.activity_show_account); + + + SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); imageLoader = ImageLoader.getInstance(); statuses = new ArrayList<>(); @@ -109,7 +120,7 @@ public class ShowAccountActivity extends AppCompatActivity implements OnPostActi accountId = b.getString("accountId"); new RetrieveRelationshipAsyncTask(getApplicationContext(), accountId,ShowAccountActivity.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); new RetrieveAccountAsyncTask(getApplicationContext(),accountId, ShowAccountActivity.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - String userId = sharedpreferences.getString(Helper.PREF_KEY_ID, null); + userId = sharedpreferences.getString(Helper.PREF_KEY_ID, null); if( accountId != null && accountId.equals(userId)){ account_follow.setVisibility(View.GONE); } @@ -164,6 +175,30 @@ public class ShowAccountActivity extends AppCompatActivity implements OnPostActi } }); + + + account_note = (TextView) findViewById(R.id.account_note); + //Register LocalBroadcast to receive selected accounts after search + hide_header = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + boolean hide = intent.getBooleanExtra("hide", false); + if( hide){ + account_follow.setVisibility(View.GONE); + account_note.setVisibility(View.GONE); + tabLayout.setVisibility(View.GONE); + }else { + account_follow.setVisibility(View.VISIBLE); + if( accountId != null && accountId.equals(userId)){ + account_follow.setVisibility(View.GONE); + } + tabLayout.setVisibility(View.VISIBLE); + account_note.setVisibility(View.VISIBLE); + } + } + }; + LocalBroadcastManager.getInstance(this).registerReceiver(hide_header, new IntentFilter(Helper.HEADER_ACCOUNT)); + //Follow button account_follow.setOnClickListener(new View.OnClickListener() { @Override @@ -215,9 +250,14 @@ public class ShowAccountActivity extends AppCompatActivity implements OnPostActi account_ac.setVisibility(View.GONE); else account_ac.setText(account.getAcct()); - tabLayout.getTabAt(0).setText(getString(R.string.status) + "\n" + String.valueOf(account.getStatuses_count())); - tabLayout.getTabAt(1).setText(getString(R.string.following) + "\n" + String.valueOf(account.getFollowing_count())); - tabLayout.getTabAt(2).setText(getString(R.string.followers) + "\n" + String.valueOf(account.getFollowers_count())); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + account_note.setText(Html.fromHtml(account.getNote(), Html.FROM_HTML_MODE_COMPACT)); + else + //noinspection deprecation + account_note.setText(Html.fromHtml(account.getNote())); + tabLayout.getTabAt(0).setText(getString(R.string.status_cnt, account.getStatuses_count())); + tabLayout.getTabAt(1).setText(getString(R.string.following_cnt, account.getFollowing_count())); + tabLayout.getTabAt(2).setText(getString(R.string.followers_cnt, account.getFollowers_count())); imageLoader.displayImage(account.getAvatar(), account_pp, options); } } @@ -232,6 +272,12 @@ public class ShowAccountActivity extends AppCompatActivity implements OnPostActi } } + @Override + public void onDestroy(){ + super.onDestroy(); + LocalBroadcastManager.getInstance(this).unregisterReceiver(hide_header); + } + @Override public void onRetrieveRelationship(Relationship relationship) { if( relationship.isBlocking()){ @@ -280,18 +326,21 @@ public class ShowAccountActivity extends AppCompatActivity implements OnPostActi DisplayStatusFragment displayStatusFragment = new DisplayStatusFragment(); bundle.putSerializable("type", RetrieveFeedsAsyncTask.Type.USER); bundle.putString("targetedId", accountId); + bundle.putBoolean("hideHeader",true); displayStatusFragment.setArguments(bundle); return displayStatusFragment; case 1: DisplayAccountsFragment displayAccountsFragment = new DisplayAccountsFragment(); bundle.putSerializable("type", RetrieveAccountsAsyncTask.Type.FOLLOWING); bundle.putString("targetedId", accountId); + bundle.putBoolean("hideHeader",true); displayAccountsFragment.setArguments(bundle); return displayAccountsFragment; case 2: displayAccountsFragment = new DisplayAccountsFragment(); bundle.putSerializable("type", RetrieveAccountsAsyncTask.Type.FOLLOWERS); bundle.putString("targetedId", accountId); + bundle.putBoolean("hideHeader",true); displayAccountsFragment.setArguments(bundle); return displayAccountsFragment; } diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/activities/WebviewActivity.java b/app/src/main/java/fr/gouv/etalab/mastodon/activities/WebviewActivity.java deleted file mode 100644 index 6f5afcb85..000000000 --- a/app/src/main/java/fr/gouv/etalab/mastodon/activities/WebviewActivity.java +++ /dev/null @@ -1,195 +0,0 @@ -/* Copyright 2017 Thomas Schneider - * - * This file is a part of Mastodon Etalab for mastodon.etalab.gouv.fr - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Mastodon Etalab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Thomas Schneider; if not, - * see . */ - -package fr.gouv.etalab.mastodon.activities; - - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -import android.webkit.CookieManager; -import android.webkit.CookieSyncManager; -import android.webkit.WebChromeClient; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.widget.ProgressBar; - -import com.loopj.android.http.AsyncHttpResponseHandler; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.HashMap; - -import cz.msebera.android.httpclient.Header; -import mastodon.etalab.gouv.fr.mastodon.R; -import fr.gouv.etalab.mastodon.asynctasks.UpdateAccountInfoAsyncTask; -import fr.gouv.etalab.mastodon.client.OauthClient; -import fr.gouv.etalab.mastodon.helper.Helper; - -/** - * Created by Thomas on 24/04/2017. - * Webview to connect accounts - */ -public class WebviewActivity extends AppCompatActivity { - - - private Activity activity; - private WebView webView; - private Context context; - private AlertDialog alert; - private String clientId, clientSecret; - - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_webview); - this.activity = this; - this.context = this; - this.context = this.getApplicationContext(); - SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); - clientId = sharedpreferences.getString(Helper.CLIENT_ID, null); - clientSecret = sharedpreferences.getString(Helper.CLIENT_SECRET, null); - - webView = (WebView) findViewById(R.id.webviewConnect); - clearCookies(getApplicationContext()); - final ProgressBar pbar = (ProgressBar) findViewById(R.id.progress_bar); - - webView.setWebChromeClient(new WebChromeClient() { - @Override - public void onProgressChanged(WebView view, int progress) { - if (progress < 100 && pbar.getVisibility() == ProgressBar.GONE) { - pbar.setVisibility(ProgressBar.VISIBLE); - } - pbar.setProgress(progress); - if (progress == 100) { - pbar.setVisibility(ProgressBar.GONE); - } - } - }); - webView.setWebViewClient(new WebViewClient() { - @SuppressWarnings("deprecation") - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url){ - super.shouldOverrideUrlLoading(view,url); - if( url.contains(Helper.REDIRECT_CONTENT)){ - String val[] = url.split("code="); - String code = val[1]; - - String action = "/oauth/token"; - HashMap parameters = new HashMap<>(); - parameters.put(Helper.CLIENT_ID, clientId); - parameters.put(Helper.CLIENT_SECRET, clientSecret); - parameters.put(Helper.REDIRECT_URI,"https://" + Helper.INSTANCE + Helper.REDIRECT_CONTENT); - parameters.put("grant_type", "authorization_code"); - parameters.put("code",code); - new OauthClient().post(action, parameters, new AsyncHttpResponseHandler() { - @Override - public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) { - String response = new String(responseBody); - JSONObject resobj; - try { - resobj = new JSONObject(response); - String token = resobj.get("access_token").toString(); - SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sharedpreferences.edit(); - editor.putString(Helper.PREF_KEY_OAUTH_TOKEN, token); - editor.apply(); - //Update the account with the token; - new UpdateAccountInfoAsyncTask(WebviewActivity.this, true, token).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } catch (JSONException e) { - e.printStackTrace(); - } - - } - - @Override - public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) { - error.printStackTrace(); - } - }); - - - return true; - } - return false; - } - }); - webView.loadUrl(redirectUserToAuthorizeAndLogin()); - } - - - @Override - public void onBackPressed() { - if (webView != null && webView.canGoBack()) { - webView.goBack(); - } else { - super.onBackPressed(); - } - } - - - - private String redirectUserToAuthorizeAndLogin() { - - String queryString = Helper.CLIENT_ID + "="+ clientId; - queryString += "&" + Helper.REDIRECT_URI + "="+ Uri.encode("https://" + Helper.INSTANCE + "/redirect_mastodon_api"); - queryString += "&" + Helper.RESPONSE_TYPE +"=code"; - queryString += "&" + Helper.SCOPE +"=" + Helper.OAUTH_SCOPES; - /*try { - queryString = URLEncoder.encode(queryString, "utf-8"); - } catch (UnsupportedEncodingException ignored) {}*/ - return "https://" + Helper.INSTANCE + Helper.EP_AUTHORIZE + "?" + queryString; - } - - - private String getOauthRedirectUri() { - return Helper.OAUTH_SCHEME + "://" + Helper.OAUTH_REDIRECT_HOST + "/"; - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (alert != null) { - alert.dismiss(); - alert = null; - } - } - - @SuppressWarnings("deprecation") - public static void clearCookies(Context context) - { - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - CookieManager.getInstance().removeAllCookies(null); - CookieManager.getInstance().flush(); - } else - { - CookieSyncManager cookieSyncMngr=CookieSyncManager.createInstance(context); - cookieSyncMngr.startSync(); - CookieManager cookieManager=CookieManager.getInstance(); - cookieManager.removeAllCookie(); - cookieManager.removeSessionCookie(); - cookieSyncMngr.stopSync(); - cookieSyncMngr.sync(); - } - } -} diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/RetrieveFeedsAsyncTask.java b/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/RetrieveFeedsAsyncTask.java index 0e6565bc9..10fae3b05 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/RetrieveFeedsAsyncTask.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/RetrieveFeedsAsyncTask.java @@ -16,10 +16,12 @@ package fr.gouv.etalab.mastodon.asynctasks; import android.content.Context; import android.os.AsyncTask; +import android.util.Log; import java.util.List; import fr.gouv.etalab.mastodon.client.API; +import fr.gouv.etalab.mastodon.helper.Helper; import fr.gouv.etalab.mastodon.interfaces.OnRetrieveFeedsInterface; diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/RetrieveHomeTimelineServiceAsyncTask.java b/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/RetrieveHomeTimelineServiceAsyncTask.java new file mode 100644 index 000000000..1abc000f3 --- /dev/null +++ b/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/RetrieveHomeTimelineServiceAsyncTask.java @@ -0,0 +1,57 @@ +/* Copyright 2017 Thomas Schneider + * + * This file is a part of Mastodon Etalab for mastodon.etalab.gouv.fr + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Mastodon Etalab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Thomas Schneider; if not, + * see . */ +package fr.gouv.etalab.mastodon.asynctasks; + +import android.content.Context; +import android.os.AsyncTask; + +import java.util.List; + +import fr.gouv.etalab.mastodon.client.API; +import fr.gouv.etalab.mastodon.interfaces.OnRetrieveHomeTimelineServiceInterface; + +/** + * Created by Thomas on 20/05/2017. + * Retrieves home timeline for the authenticated user - used in the service + */ + +public class RetrieveHomeTimelineServiceAsyncTask extends AsyncTask { + + private Context context; + private List statuses; + private String since_id; + private String acct; + private OnRetrieveHomeTimelineServiceInterface listener; + + + public RetrieveHomeTimelineServiceAsyncTask(Context context, String since_id, String acct, OnRetrieveHomeTimelineServiceInterface onRetrieveHomeTimelineServiceInterface){ + this.context = context; + this.since_id = since_id; + this.listener = onRetrieveHomeTimelineServiceInterface; + this.acct = acct; + } + + @Override + protected Void doInBackground(Void... params) { + statuses = new API(context).getHomeTimelineSinceId(since_id); + return null; + } + + @Override + protected void onPostExecute(Void result) { + listener.onRetrieveHomeTimelineService(statuses, acct); + } + +} diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/UpdateAccountInfoAsyncTask.java b/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/UpdateAccountInfoAsyncTask.java index a39442654..84ea7437f 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/UpdateAccountInfoAsyncTask.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/UpdateAccountInfoAsyncTask.java @@ -20,13 +20,11 @@ import android.content.Intent; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; import android.os.AsyncTask; -import android.util.Log; import fr.gouv.etalab.mastodon.activities.MainActivity; import fr.gouv.etalab.mastodon.client.API; import fr.gouv.etalab.mastodon.client.Entities.Account; import fr.gouv.etalab.mastodon.helper.Helper; -import fr.gouv.etalab.mastodon.interfaces.OnUpdateAccountInfoInterface; import fr.gouv.etalab.mastodon.sqlite.Sqlite; import fr.gouv.etalab.mastodon.sqlite.AccountDAO; @@ -39,22 +37,11 @@ public class UpdateAccountInfoAsyncTask extends AsyncTask { private Context context; private String token; - private boolean fromWebview; - private boolean error; - private OnUpdateAccountInfoInterface listener; - public UpdateAccountInfoAsyncTask(Context context, String token, OnUpdateAccountInfoInterface onUpdateAccountInfoInterface){ + + public UpdateAccountInfoAsyncTask(Context context, String token){ this.context = context; this.token = token; - this.fromWebview = false; - this.error = false; - this.listener = onUpdateAccountInfoInterface; - } - - public UpdateAccountInfoAsyncTask(Context context, boolean fromWebview, String token){ - this.context = context; - this.token = token; - this.fromWebview = fromWebview; } @@ -78,8 +65,6 @@ public class UpdateAccountInfoAsyncTask extends AsyncTask { else { if( account.getUsername() != null && account.getCreated_at() != null) new AccountDAO(context, db).insertAccount(account); - else //Here the user credential in db doesn't match the remote one (it will be disconnected) - error = true; } return null; } @@ -87,14 +72,10 @@ public class UpdateAccountInfoAsyncTask extends AsyncTask { @Override protected void onPostExecute(Void result) { - if( fromWebview){ - Intent mainActivity = new Intent(context, MainActivity.class); - mainActivity.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(mainActivity); - ((Activity) context).finish(); - }else{ - listener.onUpdateAccountInfo(error); - } + Intent mainActivity = new Intent(context, MainActivity.class); + mainActivity.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(mainActivity); + ((Activity) context).finish(); } diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/client/API.java b/app/src/main/java/fr/gouv/etalab/mastodon/client/API.java index c49e7e1a9..b1f403286 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/client/API.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/client/API.java @@ -16,7 +16,6 @@ package fr.gouv.etalab.mastodon.client; import android.content.Context; import android.content.SharedPreferences; -import android.util.Log; import com.loopj.android.http.AsyncHttpResponseHandler; import com.loopj.android.http.JsonHttpResponseHandler; @@ -29,6 +28,10 @@ import org.json.JSONObject; import java.io.InputStream; import java.lang.*; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; import java.util.ArrayList; import java.util.List; @@ -42,6 +45,8 @@ import fr.gouv.etalab.mastodon.client.Entities.Notification; import fr.gouv.etalab.mastodon.client.Entities.Relationship; import fr.gouv.etalab.mastodon.client.Entities.Status; +import static fr.gouv.etalab.mastodon.helper.Helper.USER_AGENT; + /** * Created by Thomas on 23/04/2017. @@ -66,7 +71,6 @@ public class API { private List notifications; private int tootPerPage, accountPerPage, notificationPerPage; private int actionCode; - private String userId; public enum StatusAction{ FAVOURITE, @@ -90,7 +94,6 @@ public class API { tootPerPage = sharedpreferences.getInt(Helper.SET_TOOTS_PER_PAGE, 40); accountPerPage = sharedpreferences.getInt(Helper.SET_ACCOUNTS_PER_PAGE, 40); notificationPerPage = sharedpreferences.getInt(Helper.SET_NOTIFICATIONS_PER_PAGE, 40); - userId = sharedpreferences.getString(Helper.PREF_KEY_ID, null); } /*** @@ -296,13 +299,7 @@ public class API { return statusContext; } - /** - * Retrieves home timeline for the account *synchronously* - * @return List - */ - public List getHomeTimeline() { - return getHomeTimeline(null, null, tootPerPage); - } + /** * Retrieves home timeline for the account *synchronously* @@ -313,6 +310,14 @@ public class API { return getHomeTimeline(max_id, null, tootPerPage); } + /** + * Retrieves home timeline for the account since an Id value *synchronously* + * @return List + */ + public List getHomeTimelineSinceId(String since_id) { + return getHomeTimeline(null, since_id, tootPerPage); + } + /** * Retrieves home timeline for the account *synchronously* * @param max_id String id max @@ -331,8 +336,7 @@ public class API { limit = 40; params.put("limit",String.valueOf(limit)); statuses = new ArrayList<>(); - - get(String.format("/accounts/%s/statuses",userId), params, new JsonHttpResponseHandler() { + get("/timelines/home", params, new JsonHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, JSONObject response) { @@ -351,14 +355,6 @@ public class API { return statuses; } - /** - * Retrieves public timeline for the account *synchronously* - * @param local boolean only local timeline - * @return List - */ - public List getPublicTimeline(boolean local){ - return getPublicTimeline(local, null, null, tootPerPage); - } /** * Retrieves public timeline for the account *synchronously* @@ -669,10 +665,8 @@ public class API { if( status.getIn_reply_to_id() != null) params.put("in_reply_to_id", status.getIn_reply_to_id()); if( status.getMedia_attachments() != null && status.getMedia_attachments().size() > 0 ) { - int i = 0; for(Attachment attachment: status.getMedia_attachments()) { params.add("media_ids[]", attachment.getId()); - i++; } } if( status.isSensitive()) @@ -703,7 +697,7 @@ public class API { } }); }else{ - delete(action, params, new JsonHttpResponseHandler() { + delete(action, null, new JsonHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, JSONObject response) { @@ -1162,25 +1156,43 @@ public class API { private void get(String action, RequestParams params, AsyncHttpResponseHandler responseHandler) { - client.setTimeout(5000); + client.setTimeout(10000); SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); String prefKeyOauthTokenT = sharedpreferences.getString(Helper.PREF_KEY_OAUTH_TOKEN, null); client.addHeader("Authorization", "Bearer "+prefKeyOauthTokenT); - client.get(getAbsoluteUrl(action), params, responseHandler); + try { + client.setUserAgent(USER_AGENT); + client.setSSLSocketFactory(new MastalabSSLSocketFactory(MastalabSSLSocketFactory.getKeystore())); + client.get(getAbsoluteUrl(action), params, responseHandler); + } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | UnrecoverableKeyException e) { + e.printStackTrace(); + } } private void post(String action, RequestParams params, AsyncHttpResponseHandler responseHandler) { SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); String prefKeyOauthTokenT = sharedpreferences.getString(Helper.PREF_KEY_OAUTH_TOKEN, null); client.addHeader("Authorization", "Bearer "+prefKeyOauthTokenT); - client.post(getAbsoluteUrl(action), params, responseHandler); + try { + client.setUserAgent(USER_AGENT); + client.setSSLSocketFactory(new MastalabSSLSocketFactory(MastalabSSLSocketFactory.getKeystore())); + client.post(getAbsoluteUrl(action), params, responseHandler); + } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | UnrecoverableKeyException e) { + e.printStackTrace(); + } } private void delete(String action, RequestParams params, AsyncHttpResponseHandler responseHandler){ SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); String prefKeyOauthTokenT = sharedpreferences.getString(Helper.PREF_KEY_OAUTH_TOKEN, null); client.addHeader("Authorization", "Bearer "+prefKeyOauthTokenT); - client.delete(getAbsoluteUrl(action), params, responseHandler); + try { + client.setUserAgent(USER_AGENT); + client.setSSLSocketFactory(new MastalabSSLSocketFactory(MastalabSSLSocketFactory.getKeystore())); + client.delete(getAbsoluteUrl(action), params, responseHandler); + } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | UnrecoverableKeyException e) { + e.printStackTrace(); + } } private String getAbsoluteUrl(String action) { diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/client/MastalabSSLSocketFactory.java b/app/src/main/java/fr/gouv/etalab/mastodon/client/MastalabSSLSocketFactory.java new file mode 100644 index 000000000..1789a491f --- /dev/null +++ b/app/src/main/java/fr/gouv/etalab/mastodon/client/MastalabSSLSocketFactory.java @@ -0,0 +1,74 @@ +package fr.gouv.etalab.mastodon.client; + +/** + * Created by Thomas on 20/05/2017. + * Custom MySSLSocketFactory + */ + + +import java.io.IOException; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + + +/** + * This file is introduced to fix HTTPS Post bug on API < ICS see + * https://code.google.com/p/android/issues/detail?id=13117#c14

 

Warning! This omits SSL + * certificate validation on every device, use with caution + */ +public class MastalabSSLSocketFactory extends com.loopj.android.http.MySSLSocketFactory { + + private final SSLContext sslContext = SSLContext.getInstance("TLS"); + /** + * Creates a new SSL Socket Factory with the given KeyStore. + * + * @param truststore A KeyStore to create the SSL Socket Factory in context of + * @throws NoSuchAlgorithmException NoSuchAlgorithmException + * @throws KeyManagementException KeyManagementException + * @throws KeyStoreException KeyStoreException + * @throws UnrecoverableKeyException UnrecoverableKeyException + */ + public MastalabSSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + super(truststore); + X509TrustManager tm = new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + sslContext.init(null, new TrustManager[]{tm}, null); + } + + @Override + public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { + return enableTLSOnSocket(sslContext.getSocketFactory().createSocket(socket, host, port, autoClose)); + } + + @Override + public Socket createSocket() throws IOException { + return enableTLSOnSocket(sslContext.getSocketFactory().createSocket()); + } + + private Socket enableTLSOnSocket(Socket socket) { + if(socket != null && (socket instanceof SSLSocket)) { + ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.1", "TLSv1.2"}); + } + return socket; + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/client/OauthClient.java b/app/src/main/java/fr/gouv/etalab/mastodon/client/OauthClient.java index fceddcb44..262c1a0a2 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/client/OauthClient.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/client/OauthClient.java @@ -14,16 +14,24 @@ * see . */ package fr.gouv.etalab.mastodon.client; +import android.os.Build; + import com.loopj.android.http.AsyncHttpClient; import com.loopj.android.http.AsyncHttpResponseHandler; import com.loopj.android.http.RequestParams; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import fr.gouv.etalab.mastodon.helper.Helper; +import static fr.gouv.etalab.mastodon.helper.Helper.USER_AGENT; + /** * Created by Thomas on 23/04/2017. * Client to call urls @@ -37,13 +45,27 @@ public class OauthClient { public void get(String action, HashMap paramaters, AsyncHttpResponseHandler responseHandler) { client.setTimeout(5000); + client.setUserAgent(USER_AGENT); RequestParams params = hashToRequestParams(paramaters); + try { + client.setSSLSocketFactory(new MastalabSSLSocketFactory(MastalabSSLSocketFactory.getKeystore())); + client.post(getAbsoluteUrl(action), params, responseHandler); + } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | UnrecoverableKeyException e) { + e.printStackTrace(); + } client.get(getAbsoluteUrl(action), params, responseHandler); } public void post(String action, HashMap paramaters, AsyncHttpResponseHandler responseHandler) { RequestParams params = hashToRequestParams(paramaters); - client.post(getAbsoluteUrl(action), params, responseHandler); + try { + client.setConnectTimeout(30000); //30s timeout + client.setUserAgent(USER_AGENT); + client.setSSLSocketFactory(new MastalabSSLSocketFactory(MastalabSSLSocketFactory.getKeystore())); + client.post(getAbsoluteUrl(action), params, responseHandler); + } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | UnrecoverableKeyException e) { + e.printStackTrace(); + } } private String getAbsoluteUrl(String action) { diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/client/PatchBaseImageDownloader.java b/app/src/main/java/fr/gouv/etalab/mastodon/client/PatchBaseImageDownloader.java new file mode 100644 index 000000000..a80527c1d --- /dev/null +++ b/app/src/main/java/fr/gouv/etalab/mastodon/client/PatchBaseImageDownloader.java @@ -0,0 +1,127 @@ +package fr.gouv.etalab.mastodon.client; + +import android.content.Context; + +import com.nostra13.universalimageloader.core.assist.ContentLengthInputStream; +import com.nostra13.universalimageloader.core.download.BaseImageDownloader; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +/** + * Created by Thomas on 21/05/2017. + * Patch for universal image loader to support TLS 1.1+ + */ + +public class PatchBaseImageDownloader extends BaseImageDownloader { + + private SSLSocketFactory sf; + + public PatchBaseImageDownloader(Context context) { + super(context); + + initSSLSocketFactory(); + } + + private void initSSLSocketFactory() { + sf = (SSLSocketFactory) SSLSocketFactory.getDefault(); + try { + sf = new MySSLSocketFactory(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException { + HttpURLConnection conn = createConnection(imageUri, extra); + if (conn instanceof HttpsURLConnection) { + ((HttpsURLConnection) conn).setSSLSocketFactory(sf); + } + return new ContentLengthInputStream(new BufferedInputStream(conn.getInputStream(), BUFFER_SIZE), conn.getContentLength()); + } + + private static class MySSLSocketFactory extends SSLSocketFactory { + SSLContext sslContext = SSLContext.getInstance("TLS"); + + MySSLSocketFactory() throws Exception { + super(); + TrustManager tm = new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + + sslContext.init(null, new TrustManager[]{tm}, null); + } + + @Override + public String[] getDefaultCipherSuites() { + return new String[0]; + } + + @Override + public String[] getSupportedCipherSuites() { + return new String[0]; + } + + + @Override + public Socket createSocket(String host, int port) throws IOException { + return enableTLSOnSocket(sslContext.getSocketFactory().createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { + return enableTLSOnSocket(sslContext.getSocketFactory().createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return enableTLSOnSocket(sslContext.getSocketFactory().createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return enableTLSOnSocket(sslContext.getSocketFactory().createSocket(address, port, localAddress, localPort)); + } + + @Override + public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { + return enableTLSOnSocket(sslContext.getSocketFactory().createSocket(socket, host, port, autoClose)); + } + + @Override + public Socket createSocket() throws IOException { + return enableTLSOnSocket(sslContext.getSocketFactory().createSocket()); + } + + private Socket enableTLSOnSocket(Socket socket) { + if(socket != null && (socket instanceof SSLSocket)) { + ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.1", "TLSv1.2"}); + } + return socket; + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/drawers/NotificationsListAdapter.java b/app/src/main/java/fr/gouv/etalab/mastodon/drawers/NotificationsListAdapter.java index 476492543..b45a3346b 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/drawers/NotificationsListAdapter.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/drawers/NotificationsListAdapter.java @@ -171,19 +171,16 @@ public class NotificationsListAdapter extends BaseAdapter { //Adds attachment -> disabled, to enable them uncomment the line below //loadAttachments(status, holder); holder.notification_status_container.setVisibility(View.VISIBLE); - - if( !status.getIn_reply_to_account_id().equals("null") || !status.getIn_reply_to_id().equals("null") ) { - holder.notification_status_content.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(context, ShowConversationActivity.class); - Bundle b = new Bundle(); - b.putString("statusId", status.getId()); //Your id - intent.putExtras(b); //Put your id to your next Intent - context.startActivity(intent); - } - }); - } + holder.notification_status_content.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(context, ShowConversationActivity.class); + Bundle b = new Bundle(); + b.putString("statusId", status.getId()); + intent.putExtras(b); + context.startActivity(intent); + } + }); switch (status.getVisibility()){ case "public": holder.status_privacy.setImageResource(R.drawable.ic_action_globe); @@ -216,8 +213,8 @@ public class NotificationsListAdapter extends BaseAdapter { public void onClick(View v) { Intent intent = new Intent(context, TootActivity.class); Bundle b = new Bundle(); - b.putString("inReplyTo", notification.getStatus().getId()); //Your id - intent.putExtras(b); //Put your id to your next Intent + b.putString("inReplyTo", notification.getStatus().getId()); + intent.putExtras(b); context.startActivity(intent); } }); diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/drawers/StatusListAdapter.java b/app/src/main/java/fr/gouv/etalab/mastodon/drawers/StatusListAdapter.java index 91341fab8..a9003785d 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/drawers/StatusListAdapter.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/drawers/StatusListAdapter.java @@ -332,7 +332,10 @@ public class StatusListAdapter extends BaseAdapter implements OnPostActionInterf public void onClick(View v) { Intent intent = new Intent(context, ShowAccountActivity.class); Bundle b = new Bundle(); - b.putString("accountId", status.getAccount().getId()); + if( status.getReblog() == null) + b.putString("accountId", status.getAccount().getId()); + else + b.putString("accountId", status.getReblog().getAccount().getId()); intent.putExtras(b); context.startActivity(intent); } @@ -406,7 +409,7 @@ public class StatusListAdapter extends BaseAdapter implements OnPostActionInterf String url = attachment.getPreview_url(); if( url == null || url.trim().equals("")) url = attachment.getUrl(); - if( url.trim().equals("https://mastodon.etalab.gouv.fr/files/small/missing.png")) + if( url.trim().contains("missing.png")) continue; imageLoader.displayImage(url, imageView, options); imageView.setOnClickListener(new View.OnClickListener() { diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/fragments/DisplayAccountsFragment.java b/app/src/main/java/fr/gouv/etalab/mastodon/fragments/DisplayAccountsFragment.java index 2a0d3a0e4..77297226a 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/fragments/DisplayAccountsFragment.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/fragments/DisplayAccountsFragment.java @@ -14,10 +14,12 @@ package fr.gouv.etalab.mastodon.fragments; * You should have received a copy of the GNU General Public License along with Thomas Schneider; if not, * see . */ import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v4.content.LocalBroadcastManager; import android.support.v4.widget.SwipeRefreshLayout; import android.view.LayoutInflater; import android.view.View; @@ -58,6 +60,7 @@ public class DisplayAccountsFragment extends Fragment implements OnRetrieveAccou private SwipeRefreshLayout swipeRefreshLayout; private int accountPerPage; private String targetedId; + private boolean hideHeader = false; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -69,6 +72,7 @@ public class DisplayAccountsFragment extends Fragment implements OnRetrieveAccou if (bundle != null) { type = (RetrieveAccountsAsyncTask.Type) bundle.get("type"); targetedId = bundle.getString("targetedId", null); + hideHeader = bundle.getBoolean("hideHeader", false); } max_id = null; firstLoad = true; @@ -78,7 +82,7 @@ public class DisplayAccountsFragment extends Fragment implements OnRetrieveAccou swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipeContainer); SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); accountPerPage = sharedpreferences.getInt(Helper.SET_ACCOUNTS_PER_PAGE, 40); - ListView lv_accounts = (ListView) rootView.findViewById(R.id.lv_accounts); + final ListView lv_accounts = (ListView) rootView.findViewById(R.id.lv_accounts); mainLoader = (RelativeLayout) rootView.findViewById(R.id.loader); nextElementLoader = (RelativeLayout) rootView.findViewById(R.id.loading_next_accounts); @@ -109,6 +113,35 @@ public class DisplayAccountsFragment extends Fragment implements OnRetrieveAccou } }); + //Hide account header when scrolling for ShowAccountActivity + if( hideHeader ) { + lv_accounts.setOnScrollListener(new AbsListView.OnScrollListener() { + int lastFirstVisibleItem = 0; + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + if (view.getId() == lv_accounts.getId() && totalItemCount > visibleItemCount) { + final int currentFirstVisibleItem = lv_accounts.getFirstVisiblePosition(); + + if (currentFirstVisibleItem > lastFirstVisibleItem) { + Intent intent = new Intent(Helper.HEADER_ACCOUNT); + intent.putExtra("hide", true); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + } else if (currentFirstVisibleItem < lastFirstVisibleItem) { + Intent intent = new Intent(Helper.HEADER_ACCOUNT); + intent.putExtra("hide", false); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + } + lastFirstVisibleItem = currentFirstVisibleItem; + } + } + }); + } + swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { @@ -157,7 +190,6 @@ public class DisplayAccountsFragment extends Fragment implements OnRetrieveAccou } - @Override public void onRetrieveAccounts(List accounts) { diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/fragments/DisplayStatusFragment.java b/app/src/main/java/fr/gouv/etalab/mastodon/fragments/DisplayStatusFragment.java index 5f277dfe9..ded4620ad 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/fragments/DisplayStatusFragment.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/fragments/DisplayStatusFragment.java @@ -14,10 +14,12 @@ package fr.gouv.etalab.mastodon.fragments; * You should have received a copy of the GNU General Public License along with Thomas Schneider; if not, * see . */ import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v4.content.LocalBroadcastManager; import android.support.v4.widget.SwipeRefreshLayout; import android.view.LayoutInflater; import android.view.View; @@ -25,7 +27,7 @@ import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.ListView; import android.widget.RelativeLayout; -import android.widget.TextView; + import java.util.ArrayList; import java.util.List; @@ -46,7 +48,6 @@ import fr.gouv.etalab.mastodon.interfaces.OnRetrieveFeedsInterface; public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsInterface { - private TextView noAction; private boolean flag_loading; private Context context; private AsyncTask asyncTask; @@ -57,9 +58,9 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn private RelativeLayout mainLoader, nextElementLoader, textviewNoAction; private boolean firstLoad; private SwipeRefreshLayout swipeRefreshLayout; - private int tootPerPage; private String targetedId; private String tag; + private boolean hideHeader = false; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -72,6 +73,7 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn type = (RetrieveFeedsAsyncTask.Type) bundle.get("type"); targetedId = bundle.getString("targetedId", null); tag = bundle.getString("tag", null); + hideHeader = bundle.getBoolean("hideHeader", false); } max_id = null; flag_loading = true; @@ -80,10 +82,9 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn boolean isOnWifi = Helper.isOnWIFI(context); swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipeContainer); SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); - tootPerPage = sharedpreferences.getInt(Helper.SET_TOOTS_PER_PAGE, 40); int behaviorWithAttachments = sharedpreferences.getInt(Helper.SET_ATTACHMENT_ACTION, Helper.ATTACHMENT_ALWAYS); - ListView lv_status = (ListView) rootView.findViewById(R.id.lv_status); + final ListView lv_status = (ListView) rootView.findViewById(R.id.lv_status); mainLoader = (RelativeLayout) rootView.findViewById(R.id.loader); nextElementLoader = (RelativeLayout) rootView.findViewById(R.id.loading_next_status); @@ -117,6 +118,35 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn } }); + //Hide account header when scrolling for ShowAccountActivity + if( hideHeader ) { + lv_status.setOnScrollListener(new AbsListView.OnScrollListener() { + int lastFirstVisibleItem = 0; + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + if (view.getId() == lv_status.getId() && totalItemCount > visibleItemCount) { + final int currentFirstVisibleItem = lv_status.getFirstVisiblePosition(); + + if (currentFirstVisibleItem > lastFirstVisibleItem) { + Intent intent = new Intent(Helper.HEADER_ACCOUNT); + intent.putExtra("hide", true); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + } else if (currentFirstVisibleItem < lastFirstVisibleItem) { + Intent intent = new Intent(Helper.HEADER_ACCOUNT); + intent.putExtra("hide", false); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + } + lastFirstVisibleItem = currentFirstVisibleItem; + } + } + }); + } + swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { @@ -193,9 +223,7 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn } swipeRefreshLayout.setRefreshing(false); firstLoad = false; - if( statuses != null && statuses.size() < tootPerPage ) - flag_loading = true; - else - flag_loading = false; + flag_loading = false; + } } diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/fragments/SettingsNotificationsFragment.java b/app/src/main/java/fr/gouv/etalab/mastodon/fragments/SettingsNotificationsFragment.java index 9d44598cf..c46601da2 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/fragments/SettingsNotificationsFragment.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/fragments/SettingsNotificationsFragment.java @@ -51,6 +51,8 @@ public class SettingsNotificationsFragment extends Fragment { boolean notif_share = sharedpreferences.getBoolean(Helper.SET_NOTIF_SHARE, true); boolean notif_validation = sharedpreferences.getBoolean(Helper.SET_NOTIF_VALIDATION, true); boolean notif_wifi = sharedpreferences.getBoolean(Helper.SET_WIFI_ONLY, false); + boolean notif_silent = sharedpreferences.getBoolean(Helper.SET_NOTIF_SILENT, false); + boolean notif_hometimeline = sharedpreferences.getBoolean(Helper.SET_NOTIF_HOMETIMELINE, true); final CheckBox set_notif_follow = (CheckBox) rootView.findViewById(R.id.set_notif_follow); final CheckBox set_notif_follow_add = (CheckBox) rootView.findViewById(R.id.set_notif_follow_add); @@ -58,7 +60,9 @@ public class SettingsNotificationsFragment extends Fragment { final CheckBox set_notif_follow_mention = (CheckBox) rootView.findViewById(R.id.set_notif_follow_mention); final CheckBox set_notif_follow_share = (CheckBox) rootView.findViewById(R.id.set_notif_follow_share); final CheckBox set_share_validation = (CheckBox) rootView.findViewById(R.id.set_share_validation); + final CheckBox set_notif_hometimeline = (CheckBox) rootView.findViewById(R.id.set_notif_hometimeline); final SwitchCompat switchCompatWIFI = (SwitchCompat) rootView.findViewById(R.id.set_wifi_only); + final SwitchCompat switchCompatSilent = (SwitchCompat) rootView.findViewById(R.id.set_silence); set_notif_follow.setChecked(notif_follow); set_notif_follow_add.setChecked(notif_add); @@ -66,9 +70,18 @@ public class SettingsNotificationsFragment extends Fragment { set_notif_follow_mention.setChecked(notif_mention); set_notif_follow_share.setChecked(notif_share); set_share_validation.setChecked(notif_validation); + set_notif_hometimeline.setChecked(notif_hometimeline); switchCompatWIFI.setChecked(notif_wifi); + switchCompatSilent.setChecked(notif_silent); - + set_notif_hometimeline.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putBoolean(Helper.SET_NOTIF_HOMETIMELINE, set_notif_hometimeline.isChecked()); + editor.apply(); + } + }); set_notif_follow.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -128,7 +141,15 @@ public class SettingsNotificationsFragment extends Fragment { } }); - + switchCompatSilent.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + // Save the state here + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putBoolean(Helper.SET_NOTIF_SILENT, isChecked); + editor.apply(); + } + }); return rootView; } diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/helper/Helper.java b/app/src/main/java/fr/gouv/etalab/mastodon/helper/Helper.java index 32bdc16e5..3c102960f 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/helper/Helper.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/helper/Helper.java @@ -19,17 +19,25 @@ package fr.gouv.etalab.mastodon.helper; import android.app.AlertDialog; import android.app.DownloadManager; +import android.app.PendingIntent; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.media.RingtoneManager; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; import android.os.Environment; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; import android.view.WindowManager; import android.widget.Toast; +import com.loopj.android.http.BuildConfig; + import java.io.File; import java.net.InetAddress; import java.text.ParseException; @@ -38,9 +46,12 @@ import java.util.Date; import java.util.Locale; import java.util.TimeZone; +import fr.gouv.etalab.mastodon.activities.MainActivity; import mastodon.etalab.gouv.fr.mastodon.R; import fr.gouv.etalab.mastodon.client.API; +import static android.app.Notification.DEFAULT_SOUND; +import static android.app.Notification.DEFAULT_VIBRATE; import static android.content.Context.DOWNLOAD_SERVICE; @@ -77,10 +88,10 @@ public class Helper { public static final String SCOPES = "scopes"; public static final String WEBSITE = "website"; public static final String LAST_NOTIFICATION_MAX_ID = "last_notification_max_id"; - + public static final String LAST_HOMETIMELINE_MAX_ID = "last_hometimeline_max_id"; //Notifications - public static final String NOTIFICATION_TYPE = "notification_type"; public static final int NOTIFICATION_INTENT = 1; + public static final int HOME_TIMELINE_INTENT = 2; //Settings public static final String SET_TOOTS_PER_PAGE = "set_toots_per_page"; @@ -99,6 +110,7 @@ public class Helper { public static final String SET_NOTIF_SHARE = "set_notif_follow_share"; public static final String SET_NOTIF_VALIDATION = "set_share_validation"; public static final String SET_WIFI_ONLY = "set_wifi_only"; + public static final String SET_NOTIF_HOMETIMELINE = "set_notif_hometimeline"; public static final String SET_NOTIF_SILENT = "set_notif_silent"; //End points @@ -107,16 +119,17 @@ public class Helper { //Refresh job public static final int MINUTES_BETWEEN_NOTIFICATIONS_REFRESH = 15; + public static final int MINUTES_BETWEEN_HOME_TIMELINE = 30; //Intent public static final String INTENT_ACTION = "intent_action"; - public static final int INTENT_NOTIFICATION = 1; //Receiver public static final String SEARCH_VALIDATE_ACCOUNT = "search_validate_account"; + public static final String HEADER_ACCOUNT = "header_account"; - - + //User agent + public static final String USER_AGENT = "Mastalab/"+ BuildConfig.VERSION_NAME + " Android/"+ Build.VERSION.RELEASE; /*** * Check if the user is connected to Internet @@ -364,4 +377,40 @@ public class Helper { alert.show(); } + + /** + * Sends notification with intent + * @param context Context + * @param intentAction int intent action + * @param notificationId int id of the notification + * @param icon Bitmap profile picture + * @param title String title of the notification + * @param message String message for the notification + */ + public static void notify_user(Context context, int intentAction, int notificationId, Bitmap icon, String title, String message ) { + final SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); + // prepare intent which is triggered if the user click on the notification + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + final Intent intent = new Intent(context, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK ); + intent.putExtra(INTENT_ACTION, intentAction); + PendingIntent pIntent = PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_ONE_SHOT); + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + // build notification + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context) + .setSmallIcon(R.drawable.notification_icon) + .setTicker(message) + .setWhen(System.currentTimeMillis()) + .setAutoCancel(true) + .setContentIntent(pIntent) + .setContentText(message); + if( sharedpreferences.getBoolean(Helper.SET_NOTIF_SILENT,false) ) { + notificationBuilder.setDefaults(DEFAULT_VIBRATE); + }else { + notificationBuilder.setDefaults(DEFAULT_SOUND); + } + notificationBuilder.setContentTitle(title); + notificationBuilder.setLargeIcon(icon); + notificationManager.notify(notificationId, notificationBuilder.build()); + } } diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/interfaces/OnRetrieveHomeTimelineServiceInterface.java b/app/src/main/java/fr/gouv/etalab/mastodon/interfaces/OnRetrieveHomeTimelineServiceInterface.java new file mode 100644 index 000000000..4ac7f3fe2 --- /dev/null +++ b/app/src/main/java/fr/gouv/etalab/mastodon/interfaces/OnRetrieveHomeTimelineServiceInterface.java @@ -0,0 +1,27 @@ +/* Copyright 2017 Thomas Schneider + * + * This file is a part of Mastodon Etalab for mastodon.etalab.gouv.fr + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Mastodon Etalab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Thomas Schneider; if not, + * see . */ +package fr.gouv.etalab.mastodon.interfaces; + + +import java.util.List; +import fr.gouv.etalab.mastodon.client.Entities.Status; + +/** + * Created by Thomas on 20/05/2017. + * Interface when home timeline toots have been retrieved + */ +public interface OnRetrieveHomeTimelineServiceInterface { + void onRetrieveHomeTimelineService(List statuses, String acct); +} diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/jobs/ApplicationJob.java b/app/src/main/java/fr/gouv/etalab/mastodon/jobs/ApplicationJob.java index 84c301cc3..31be81763 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/jobs/ApplicationJob.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/jobs/ApplicationJob.java @@ -28,6 +28,8 @@ public class ApplicationJob implements JobCreator { switch (tag) { case NotificationsSyncJob.NOTIFICATION_REFRESH: return new NotificationsSyncJob(); + case HomeTimelineSyncJob.HOME_TIMELINE: + return new HomeTimelineSyncJob(); default: return null; } diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/jobs/HomeTimelineSyncJob.java b/app/src/main/java/fr/gouv/etalab/mastodon/jobs/HomeTimelineSyncJob.java new file mode 100644 index 000000000..41ed06f85 --- /dev/null +++ b/app/src/main/java/fr/gouv/etalab/mastodon/jobs/HomeTimelineSyncJob.java @@ -0,0 +1,164 @@ +package fr.gouv.etalab.mastodon.jobs; +/* Copyright 2017 Thomas Schneider + * + * This file is a part of Mastodon Etalab for mastodon.etalab.gouv.fr + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Mastodon Etalab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Thomas Schneider; if not, + * see . */ + +import android.content.Context; +import android.content.SharedPreferences; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.AsyncTask; +import android.support.annotation.NonNull; + +import com.evernote.android.job.Job; +import com.evernote.android.job.JobManager; +import com.evernote.android.job.JobRequest; +import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiskCache; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; + +import java.io.File; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import fr.gouv.etalab.mastodon.asynctasks.RetrieveHomeTimelineServiceAsyncTask; +import fr.gouv.etalab.mastodon.client.Entities.Account; +import fr.gouv.etalab.mastodon.client.Entities.Status; +import fr.gouv.etalab.mastodon.client.PatchBaseImageDownloader; +import fr.gouv.etalab.mastodon.helper.Helper; +import fr.gouv.etalab.mastodon.interfaces.OnRetrieveHomeTimelineServiceInterface; +import fr.gouv.etalab.mastodon.sqlite.AccountDAO; +import fr.gouv.etalab.mastodon.sqlite.Sqlite; +import mastodon.etalab.gouv.fr.mastodon.R; + +import static fr.gouv.etalab.mastodon.helper.Helper.HOME_TIMELINE_INTENT; +import static fr.gouv.etalab.mastodon.helper.Helper.notify_user; + + +/** + * Created by Thomas on 20/05/2017. + * Notifications for home timeline job + */ + +public class HomeTimelineSyncJob extends Job implements OnRetrieveHomeTimelineServiceInterface{ + + static final String HOME_TIMELINE = "home_timeline"; + private int notificationId; + + @NonNull + @Override + protected Result onRunJob(Params params) { + callAsynchronousTask(); + return Result.SUCCESS; + } + + + public static int schedule(boolean updateCurrent){ + + Set jobRequests = JobManager.instance().getAllJobRequestsForTag(HOME_TIMELINE); + if (!jobRequests.isEmpty() && !updateCurrent) { + return jobRequests.iterator().next().getJobId(); + } + return new JobRequest.Builder(HomeTimelineSyncJob.HOME_TIMELINE) + .setPeriodic(TimeUnit.MINUTES.toMillis(Helper.MINUTES_BETWEEN_HOME_TIMELINE), TimeUnit.MINUTES.toMillis(5)) + .setPersisted(true) + .setUpdateCurrent(updateCurrent) + .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) + .setRequirementsEnforced(false) + .build() + .schedule(); + } + + + /** + * Task in background starts here. + */ + private void callAsynchronousTask() { + final SharedPreferences sharedpreferences = getContext().getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); + boolean notif_hometimeline = sharedpreferences.getBoolean(Helper.SET_NOTIF_HOMETIMELINE, true); + //User disagree with home timeline refresh + if( !notif_hometimeline) + return; //Nothing is done + SQLiteDatabase db = Sqlite.getInstance(getContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); + //If an Internet connection and user agrees with notification refresh + //If WIFI only and on WIFI OR user defined any connections to use the service. + if(!sharedpreferences.getBoolean(Helper.SET_WIFI_ONLY, false) || Helper.isOnWIFI(getContext())) { + List accounts = new AccountDAO(getContext(),db).getAllAccount(); + //It means there is no user in DB. + if( accounts == null ) + return; + //Retrieve users in db that owner has. + for (Account account: accounts) { + String since_id = sharedpreferences.getString(Helper.LAST_HOMETIMELINE_MAX_ID + account.getAcct(), null); + notificationId = (int) Math.round(Double.parseDouble(account.getId())/1000); + new RetrieveHomeTimelineServiceAsyncTask(getContext(), since_id, account.getAcct(), HomeTimelineSyncJob.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + + } + } + } + + + @Override + public void onRetrieveHomeTimelineService(List statuses, String acct) { + if( statuses == null || statuses.size() == 0) + return; + Bitmap icon_notification = null; + final SharedPreferences sharedpreferences = getContext().getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); + + String max_id = sharedpreferences.getString(Helper.LAST_HOMETIMELINE_MAX_ID + acct, null); + //No previous notifications in cache, so no notification will be sent + if( max_id != null ){ + String message; + String title = null; + for(Status status: statuses){ + //The notification associated to max_id is discarded as it is supposed to have already been sent + if( status.getId().equals(max_id)) + continue; + String notificationUrl = status.getAccount().getAvatar(); + if( notificationUrl != null && icon_notification == null){ + try { + ImageLoader imageLoaderNoty = ImageLoader.getInstance(); + File cacheDir = new File(getContext().getCacheDir(), getContext().getString(R.string.app_name)); + ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(getContext()) + .imageDownloader(new PatchBaseImageDownloader(getContext())) + .threadPoolSize(5) + .threadPriority(Thread.MIN_PRIORITY + 3) + .denyCacheImageMultipleSizesInMemory() + .diskCache(new UnlimitedDiskCache(cacheDir)) + .build(); + imageLoaderNoty.init(config); + icon_notification = imageLoaderNoty.loadImageSync(notificationUrl); + title = getContext().getResources().getString(R.string.notif_pouet, status.getAccount().getDisplay_name()); + }catch (Exception e){ + icon_notification = BitmapFactory.decodeResource(getContext().getResources(), + R.drawable.mastodonlogo); + } + } + } + if(statuses.size() > 0 ) + message = getContext().getResources().getQuantityString(R.plurals.other_notif_hometimeline, statuses.size(), statuses.size()); + else + message = ""; + notify_user(getContext(), HOME_TIMELINE_INTENT, notificationId, icon_notification,title,message); + } + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(Helper.LAST_HOMETIMELINE_MAX_ID + acct, statuses.get(0).getId()); + editor.apply(); + } + + + +} diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/jobs/NotificationsSyncJob.java b/app/src/main/java/fr/gouv/etalab/mastodon/jobs/NotificationsSyncJob.java index 35e2df73d..84510a7e1 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/jobs/NotificationsSyncJob.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/jobs/NotificationsSyncJob.java @@ -13,18 +13,15 @@ package fr.gouv.etalab.mastodon.jobs; * * You should have received a copy of the GNU General Public License along with Thomas Schneider; if not, * see . */ -import android.app.PendingIntent; + import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.media.RingtoneManager; import android.os.AsyncTask; import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationManagerCompat; + import com.evernote.android.job.Job; import com.evernote.android.job.JobManager; @@ -39,7 +36,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; -import fr.gouv.etalab.mastodon.activities.MainActivity; +import fr.gouv.etalab.mastodon.client.PatchBaseImageDownloader; import fr.gouv.etalab.mastodon.helper.Helper; import mastodon.etalab.gouv.fr.mastodon.R; import fr.gouv.etalab.mastodon.asynctasks.RetrieveNotificationsAsyncTask; @@ -49,8 +46,8 @@ import fr.gouv.etalab.mastodon.interfaces.OnRetrieveNotificationsInterface; import fr.gouv.etalab.mastodon.sqlite.AccountDAO; import fr.gouv.etalab.mastodon.sqlite.Sqlite; -import static fr.gouv.etalab.mastodon.helper.Helper.INTENT_ACTION; -import static fr.gouv.etalab.mastodon.helper.Helper.INTENT_NOTIFICATION; +import static fr.gouv.etalab.mastodon.helper.Helper.NOTIFICATION_INTENT; +import static fr.gouv.etalab.mastodon.helper.Helper.notify_user; /** @@ -60,8 +57,7 @@ import static fr.gouv.etalab.mastodon.helper.Helper.INTENT_NOTIFICATION; public class NotificationsSyncJob extends Job implements OnRetrieveNotificationsInterface{ - public static final String NOTIFICATION_REFRESH = "job_notification"; - private int jobId; + static final String NOTIFICATION_REFRESH = "job_notification"; private int notificationId; @NonNull @@ -73,7 +69,7 @@ public class NotificationsSyncJob extends Job implements OnRetrieveNotifications } - public static int schedule(Context context, boolean updateCurrent){ + public static int schedule(boolean updateCurrent){ Set jobRequests = JobManager.instance().getAllJobRequestsForTag(NOTIFICATION_REFRESH); if (!jobRequests.isEmpty() && !updateCurrent) { @@ -99,6 +95,15 @@ public class NotificationsSyncJob extends Job implements OnRetrieveNotifications SQLiteDatabase db = Sqlite.getInstance(getContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); //If an Internet connection and user agrees with notification refresh final SharedPreferences sharedpreferences = getContext().getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); + //Check which notifications the user wants to see + boolean notif_follow = sharedpreferences.getBoolean(Helper.SET_NOTIF_FOLLOW, true); + boolean notif_add = sharedpreferences.getBoolean(Helper.SET_NOTIF_ADD, true); + boolean notif_ask = sharedpreferences.getBoolean(Helper.SET_NOTIF_ASK, true); + boolean notif_mention = sharedpreferences.getBoolean(Helper.SET_NOTIF_MENTION, true); + boolean notif_share = sharedpreferences.getBoolean(Helper.SET_NOTIF_SHARE, true); + //User disagree with all notifications + if( !notif_follow && !notif_add && !notif_ask && !notif_mention && !notif_share) + return; //Nothing is done //If WIFI only and on WIFI OR user defined any connections to use the service. if(!sharedpreferences.getBoolean(Helper.SET_WIFI_ONLY, false) || Helper.isOnWIFI(getContext())) { List accounts = new AccountDAO(getContext(),db).getAllAccount(); @@ -124,7 +129,7 @@ public class NotificationsSyncJob extends Job implements OnRetrieveNotifications final SharedPreferences sharedpreferences = getContext().getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); boolean notif_follow = sharedpreferences.getBoolean(Helper.SET_NOTIF_FOLLOW, true); boolean notif_add = sharedpreferences.getBoolean(Helper.SET_NOTIF_ADD, true); - boolean notif_ask = sharedpreferences.getBoolean(Helper.SET_NOTIF_ASK, true); + //boolean notif_ask = sharedpreferences.getBoolean(Helper.SET_NOTIF_ASK, true); boolean notif_mention = sharedpreferences.getBoolean(Helper.SET_NOTIF_MENTION, true); boolean notif_share = sharedpreferences.getBoolean(Helper.SET_NOTIF_SHARE, true); String max_id = sharedpreferences.getString(Helper.LAST_NOTIFICATION_MAX_ID + acct, null); @@ -137,7 +142,7 @@ public class NotificationsSyncJob extends Job implements OnRetrieveNotifications int newShare = 0; String notificationUrl = null; String title = null; - String message = null; + String message; for(Notification notification: notifications){ //The notification associated to max_id is discarded as it is supposed to have already been sent if( notification.getId().equals(max_id)) @@ -186,6 +191,7 @@ public class NotificationsSyncJob extends Job implements OnRetrieveNotifications ImageLoader imageLoaderNoty = ImageLoader.getInstance(); File cacheDir = new File(getContext().getCacheDir(), getContext().getString(R.string.app_name)); ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(getContext()) + .imageDownloader(new PatchBaseImageDownloader(getContext())) .threadPoolSize(5) .threadPriority(Thread.MIN_PRIORITY + 3) .denyCacheImageMultipleSizesInMemory() @@ -195,7 +201,7 @@ public class NotificationsSyncJob extends Job implements OnRetrieveNotifications icon_notification = imageLoaderNoty.loadImageSync(notificationUrl); }catch (Exception e){ icon_notification = BitmapFactory.decodeResource(getContext().getResources(), - R.drawable.mastodon_logo); + R.drawable.mastodonlogo); } } } @@ -207,7 +213,7 @@ public class NotificationsSyncJob extends Job implements OnRetrieveNotifications message = getContext().getResources().getQuantityString(R.plurals.other_notifications, other, other); else message = ""; - notify_user(icon_notification,title,message); + notify_user(getContext(), NOTIFICATION_INTENT, notificationId, icon_notification,title,message); } } SharedPreferences.Editor editor = sharedpreferences.edit(); @@ -216,36 +222,4 @@ public class NotificationsSyncJob extends Job implements OnRetrieveNotifications } - /** - * Sends notification with intent - * @param icon Bitmap profile picture - * @param title String title of the notification - * @param message String message for the notification - */ - private void notify_user(Bitmap icon, String title, String message ) { - final SharedPreferences sharedpreferences = getContext().getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); - // prepare intent which is triggered if the user click on the notification - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getContext()); - final Intent intent = new Intent(getContext(), MainActivity.class); - intent.putExtra(Helper.NOTIFICATION_TYPE, Helper.NOTIFICATION_INTENT); - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK ); - intent.putExtra(INTENT_ACTION, INTENT_NOTIFICATION); - PendingIntent pIntent = PendingIntent.getActivity(getContext(), notificationId, intent, PendingIntent.FLAG_ONE_SHOT); - - RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); - // build notification - NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getContext()) - .setSmallIcon(R.drawable.notification_icon) - .setTicker(message) - .setWhen(System.currentTimeMillis()) - .setAutoCancel(true) - .setContentIntent(pIntent) - .setContentText(message); - if( !sharedpreferences.getBoolean(Helper.SET_NOTIF_SILENT,false) ) { - notificationBuilder.setDefaults(-1); - } - notificationBuilder.setContentTitle(title); - notificationBuilder.setLargeIcon(icon); - notificationManager.notify(notificationId, notificationBuilder.build()); - } } diff --git a/app/src/main/res/drawable/background_splash.xml b/app/src/main/res/drawable/background_splash.xml index 3b4b972b7..1204da0ab 100644 --- a/app/src/main/res/drawable/background_splash.xml +++ b/app/src/main/res/drawable/background_splash.xml @@ -23,6 +23,6 @@ + android:src="@drawable/mastodonlogo"/> \ No newline at end of file diff --git a/app/src/main/res/drawable/mastodon_logo.png b/app/src/main/res/drawable/mastodon_logo.png deleted file mode 100644 index f0df29927..000000000 Binary files a/app/src/main/res/drawable/mastodon_logo.png and /dev/null differ diff --git a/app/src/main/res/drawable/mastodonlogo.png b/app/src/main/res/drawable/mastodonlogo.png new file mode 100644 index 000000000..3a2574151 Binary files /dev/null and b/app/src/main/res/drawable/mastodonlogo.png differ diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index f94e777b9..6990bbf42 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -20,6 +20,7 @@ android:layout_height="match_parent" android:orientation="vertical" > + + - + - + +