Remote instance follow - step1

This commit is contained in:
stom79 2018-08-20 19:00:20 +02:00
parent 7df2c0b59e
commit 57d30a3f2e
20 changed files with 983 additions and 10 deletions

View File

@ -63,6 +63,12 @@
android:name=".services.StreamingLocalTimelineService"
android:exported="false"/>
<activity android:name=".activities.InstanceFederatedActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:windowSoftInputMode = "adjustResize"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@style/AppThemeDark_NoActionBar"/>
<activity
android:name=".activities.MainActivity"
android:label="@string/app_name"

View File

@ -251,6 +251,16 @@ public abstract class BaseMainActivity extends BaseActivity
iconGlobal.setColorFilter(ContextCompat.getColor(getApplicationContext(), R.color.dark_text), PorterDuff.Mode.SRC_IN);
iconGlobal.setImageResource(R.drawable.ic_public);
FloatingActionButton federatedTimelines = findViewById(R.id.federated_timeline);
federatedTimelines.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(getApplicationContext(), InstanceFederatedActivity.class);
startActivity(intent);
}
});
changeDrawableColor(getApplicationContext(), R.drawable.ic_home,R.color.dark_text);
changeDrawableColor(getApplicationContext(), R.drawable.ic_notifications,R.color.dark_text);
changeDrawableColor(getApplicationContext(), R.drawable.ic_people,R.color.dark_text);

View File

@ -0,0 +1,563 @@
/* Copyright 2017 Thomas Schneider
*
* This file is a part of Mastalab
*
* 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.
*
* Mastalab 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 Mastalab; if not,
* see <http://www.gnu.org/licenses>. */
package fr.gouv.etalab.mastodon.activities;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.FloatingActionButton;
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.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import fr.gouv.etalab.mastodon.R;
import fr.gouv.etalab.mastodon.asynctasks.RetrieveFeedsAsyncTask;
import fr.gouv.etalab.mastodon.client.Entities.Account;
import fr.gouv.etalab.mastodon.client.Entities.Status;
import fr.gouv.etalab.mastodon.client.HttpsConnection;
import fr.gouv.etalab.mastodon.fragments.DisplayStatusFragment;
import fr.gouv.etalab.mastodon.helper.Helper;
import fr.gouv.etalab.mastodon.services.LiveNotificationService;
import fr.gouv.etalab.mastodon.sqlite.AccountDAO;
import fr.gouv.etalab.mastodon.sqlite.InstancesDAO;
import fr.gouv.etalab.mastodon.sqlite.Sqlite;
import static fr.gouv.etalab.mastodon.helper.Helper.INSTANCE_NAME;
import static fr.gouv.etalab.mastodon.helper.Helper.INTENT_ACTION;
import static fr.gouv.etalab.mastodon.helper.Helper.SEARCH_INSTANCE;
import static fr.gouv.etalab.mastodon.helper.Helper.THEME_BLACK;
public class InstanceFederatedActivity extends BaseActivity {
private FloatingActionButton add_new;
public static String currentLocale;
private TabLayout tabLayout;
private ViewPager viewPager;
private static BroadcastReceiver receive_data, receive_federated_data, receive_local_data;
private String userIdService;
private AppBarLayout appBar;
private String userId;
private String instance;
private PagerAdapter adapter;
boolean isLoadingInstance = false;
private AutoCompleteTextView instance_list;
private String oldSearch;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE);
final int theme = sharedpreferences.getInt(Helper.SET_THEME, Helper.THEME_DARK);
switch (theme){
case Helper.THEME_LIGHT:
setTheme(R.style.AppTheme_NoActionBar);
break;
case Helper.THEME_DARK:
setTheme(R.style.AppThemeDark_NoActionBar);
break;
case Helper.THEME_BLACK:
setTheme(R.style.AppThemeBlack_NoActionBar);
break;
default:
setTheme(R.style.AppThemeDark_NoActionBar);
}
setContentView(R.layout.activity_federated);
FloatingActionButton federated_timeline_close = findViewById(R.id.federated_timeline_close);
federated_timeline_close.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
FloatingActionButton add_new_instance = findViewById(R.id.add_new_instance);
add_new_instance.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(InstanceFederatedActivity.this);
LayoutInflater inflater = getLayoutInflater();
@SuppressLint("InflateParams") View dialogView = inflater.inflate(R.layout.search_instance, null);
dialogBuilder.setView(dialogView);
instance_list = dialogView.findViewById(R.id.search_instance);
instance_list.setFilters(new InputFilter[]{new InputFilter.LengthFilter(60)});
dialogBuilder.setPositiveButton(R.string.validate, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
SQLiteDatabase db = Sqlite.getInstance(getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
String instanceName = instance_list.getText().toString().trim();
//Already in db
List<String> s_ = new InstancesDAO(InstanceFederatedActivity.this, db).getInstanceByName(instanceName);
if( s_ == null)
s_ = new ArrayList<>();
new Thread(new Runnable(){
@Override
public void run() {
try {
String response = new HttpsConnection(InstanceFederatedActivity.this).get("https://" + instanceName + "/api/v1/timelines/public?local=true", 10, null, null);
runOnUiThread(new Runnable() {
public void run() {
JSONObject resobj;
try {
new InstancesDAO(InstanceFederatedActivity.this, db).insertInstance(instanceName);
resobj = new JSONObject(response);
Intent intent = new Intent(InstanceFederatedActivity.this, InstanceFederatedActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra(INTENT_ACTION, SEARCH_INSTANCE);
intent.putExtra(INSTANCE_NAME, instanceName);
startActivity(intent);
} catch (JSONException ignored) {ignored.printStackTrace();}
}
});
} catch (final Exception e) {
e.printStackTrace();
runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(getApplicationContext(), R.string.toast_instance_unavailable,Toast.LENGTH_LONG).show();
}
});
}
}
}).start();
}
});
AlertDialog alertDialog = dialogBuilder.create();
alertDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialogInterface) {
//Hide keyboard
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
assert imm != null;
imm.hideSoftInputFromWindow(instance_list.getWindowToken(), 0);
}
});
if( alertDialog.getWindow() != null )
alertDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
alertDialog.show();
instance_list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick (AdapterView<?> parent, View view, int position, long id) {
oldSearch = parent.getItemAtPosition(position).toString().trim();
}
});
instance_list.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
Pattern host = Pattern.compile("([\\da-z\\.-]+\\.[a-z\\.]{2,12})");
Matcher matcher = host.matcher(s.toString().trim());
if( s.toString().trim().length() == 0 || !matcher.find()) {
alertDialog.getButton(
AlertDialog.BUTTON_POSITIVE).setEnabled(false);
} else {
// Something into edit text. Enable the button.
alertDialog.getButton(
AlertDialog.BUTTON_POSITIVE).setEnabled(true);
}
if (s.length() > 2 && !isLoadingInstance) {
final String action = "/instances/search";
final HashMap<String, String> parameters = new HashMap<>();
parameters.put("q", s.toString().trim());
parameters.put("count", String.valueOf(50));
parameters.put("name", String.valueOf(true));
isLoadingInstance = true;
if( oldSearch == null || !oldSearch.equals(s.toString().trim()))
new Thread(new Runnable() {
@Override
public void run() {
try {
final String response = new HttpsConnection(InstanceFederatedActivity.this).get("https://instances.social/api/1.0" + action, 30, parameters, Helper.THEKINRAR_SECRET_TOKEN);
runOnUiThread(new Runnable() {
public void run() {
isLoadingInstance = false;
String[] instances;
try {
JSONObject jsonObject = new JSONObject(response);
JSONArray jsonArray = jsonObject.getJSONArray("instances");
if (jsonArray != null) {
int length = 0;
for (int i = 0; i < jsonArray.length(); i++) {
if( !jsonArray.getJSONObject(i).get("name").toString().contains("@"))
length++;
}
instances = new String[length];
int j = 0;
for (int i = 0; i < jsonArray.length(); i++) {
if( !jsonArray.getJSONObject(i).get("name").toString().contains("@")) {
instances[j] = jsonArray.getJSONObject(i).get("name").toString();
j++;
}
}
} else {
instances = new String[]{};
}
instance_list.setAdapter(null);
ArrayAdapter<String> adapter =
new ArrayAdapter<>(InstanceFederatedActivity.this, android.R.layout.simple_list_item_1, instances);
instance_list.setAdapter(adapter);
if (instance_list.hasFocus() && !InstanceFederatedActivity.this.isFinishing())
instance_list.showDropDown();
oldSearch = s.toString().trim();
} catch (JSONException ignored) {
isLoadingInstance = false;
}
}
});
} catch (HttpsConnection.HttpsConnectionException e) {
isLoadingInstance = false;
} catch (Exception e) {
isLoadingInstance = false;
}
}
}).start();
else
isLoadingInstance = false;
}
}
});
}
});
//Test if user is still log in
if( ! Helper.isLoggedIn(getApplicationContext())) {
//It is not, the user is redirected to the login page
Intent myIntent = new Intent(InstanceFederatedActivity.this, LoginActivity.class);
startActivity(myIntent);
finish();
return;
}
SQLiteDatabase db = Sqlite.getInstance(getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
Helper.canPin = false;
Helper.fillMapEmoji(getApplicationContext());
//Here, the user is authenticated
appBar = findViewById(R.id.appBar);
Toolbar toolbar = findViewById(R.id.toolbar);
if( theme == THEME_BLACK)
toolbar.setBackgroundColor(ContextCompat.getColor(InstanceFederatedActivity.this, R.color.black));
setSupportActionBar(toolbar);
tabLayout = findViewById(R.id.tabLayout);
tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
tabLayout.setTabMode(TabLayout.MODE_FIXED);
//Display filter for notification when long pressing the tab
final LinearLayout tabStrip = (LinearLayout) tabLayout.getChildAt(0);
viewPager = findViewById(R.id.viewpager);
adapter = new PagerAdapter
(getSupportFragmentManager(), tabLayout.getTabCount());
viewPager.setAdapter(adapter);
viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
refreshInstanceTab();
//Hide the default title
if( getSupportActionBar() != null) {
getSupportActionBar().setDisplayShowTitleEnabled(false);
getSupportActionBar().getThemedContext().setTheme(R.style.AppThemeBlack);
}
//Defines the current locale of the device in a static variable
currentLocale = Helper.currentLocale(getApplicationContext());
add_new = findViewById(R.id.add_new);
userId = sharedpreferences.getString(Helper.PREF_KEY_ID, null);
instance = sharedpreferences.getString(Helper.PREF_INSTANCE, Helper.getLiveInstance(getApplicationContext()));
Account account = new AccountDAO(getApplicationContext(), db).getAccountByID(userId);
if( account == null){
Helper.logout(getApplicationContext());
Intent myIntent = new Intent(InstanceFederatedActivity.this, LoginActivity.class);
startActivity(myIntent);
finish();
return;
}
ImageView iconbar = toolbar.findViewById(R.id.iconbar);
Helper.loadPictureIcon(InstanceFederatedActivity.this, account.getAvatar(),iconbar);
if( receive_data != null)
LocalBroadcastManager.getInstance(this).unregisterReceiver(receive_data);
receive_data = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Bundle b = intent.getExtras();
Helper.EventStreaming eventStreaming = (Helper.EventStreaming) intent.getSerializableExtra("eventStreaming");
assert b != null;
userIdService = b.getString("userIdService", null);
if( userIdService != null && userIdService.equals(userId)) {
}
}
};
mamageNewIntent(getIntent());
// LocalBroadcastManager.getInstance(this).registerReceiver(receive_data, new IntentFilter(Helper.RECEIVE_DATA));
}
public void refreshInstanceTab(){
Helper.addInstanceTab(InstanceFederatedActivity.this, tabLayout, adapter);
}
@Override
public void onResume(){
super.onResume();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
mamageNewIntent(intent);
}
/**
* Manages new intents
* @param intent Intent - intent related to a notification in top bar
*/
private void mamageNewIntent(Intent intent){
if( intent == null || intent.getExtras() == null )
return;
Bundle extras = intent.getExtras();
if( extras.containsKey(INTENT_ACTION) ){
if(extras.getInt(INTENT_ACTION) == SEARCH_INSTANCE){
String instanceName = extras.getString(INSTANCE_NAME);
if( instanceName != null){
adapter = new InstanceFederatedActivity.PagerAdapter
(getSupportFragmentManager(), tabLayout.getTabCount());
viewPager.setAdapter(adapter);
for(int i = 0; i < tabLayout.getTabCount() ; i++ ){
if( tabLayout.getTabAt(i).getText() != null && tabLayout.getTabAt(i).getText().equals(instanceName.trim())){
tabLayout.getTabAt(i).select();
break;
}
}
}
}
}
intent.replaceExtras(new Bundle());
intent.setAction("");
intent.setData(null);
intent.setFlags(0);
}
@Override
public void onStart(){
super.onStart();
if( receive_federated_data != null)
LocalBroadcastManager.getInstance(this).unregisterReceiver(receive_federated_data);
receive_federated_data = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Bundle b = intent.getExtras();
assert b != null;
userIdService = b.getString("userIdService", null);
if( userIdService != null && userIdService.equals(userId)) {
Status status = b.getParcelable("data");
}
}
};
LocalBroadcastManager.getInstance(this).registerReceiver(receive_federated_data, new IntentFilter(Helper.RECEIVE_FEDERATED_DATA));
}
@Override
public void onStop(){
super.onStop();
if( receive_federated_data != null)
LocalBroadcastManager.getInstance(this).unregisterReceiver(receive_federated_data);
}
@Override
protected void onPause() {
super.onPause();
}
@Override
public void onDestroy(){
super.onDestroy();
if( receive_data != null)
LocalBroadcastManager.getInstance(this).unregisterReceiver(receive_data);
}
/**
* Page Adapter for settings
*/
public class PagerAdapter extends FragmentStatePagerAdapter {
int mNumOfTabs;
private PagerAdapter(FragmentManager fm, int NumOfTabs) {
super(fm);
this.mNumOfTabs = NumOfTabs;
}
public void removeTabPage() {
this.mNumOfTabs--;
notifyDataSetChanged();
}
public void addTabPage(String title) {
TabLayout.Tab tab = tabLayout.newTab();
tab.setText(title);
this.mNumOfTabs++;
notifyDataSetChanged();
}
@Override
public Fragment getItem(int position) {
//Selection comes from another menu, no action to do
DisplayStatusFragment statusFragment;
Bundle bundle = new Bundle();
statusFragment = new DisplayStatusFragment();
bundle.putSerializable("type", RetrieveFeedsAsyncTask.Type.REMOTE_INSTANCE);
bundle.putString("remote_instance", tabLayout.getTabAt(position).getText().toString());
statusFragment.setArguments(bundle);
return statusFragment;
}
@Override
public int getCount() {
return mNumOfTabs;
}
}
@SuppressWarnings("ConstantConditions")
public void updateTimeLine(RetrieveFeedsAsyncTask.Type type, int value){
int position = tabLayout.getSelectedTabPosition();
View tabLocal = tabLayout.getTabAt(position).getCustomView();
assert tabLocal != null;
TextView tabCounter = tabLocal.findViewById(R.id.tab_counter);
tabCounter.setText(String.valueOf(value));
if( value > 0){
tabCounter.setVisibility(View.VISIBLE);
}else {
tabCounter.setVisibility(View.GONE);
}
}
public void startSreaming(){
SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE);
boolean liveNotifications = sharedpreferences.getBoolean(Helper.SET_LIVE_NOTIFICATIONS, true);
if( liveNotifications) {
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
assert manager != null;
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
if (LiveNotificationService.class.getName().equals(service.service.getClassName())) {
return;
}
}
Intent streamingIntent = new Intent(this, LiveNotificationService.class);
startService(streamingIntent);
}
}
}

View File

@ -34,6 +34,7 @@ import android.text.util.Linkify;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
@ -72,6 +73,7 @@ public class LoginActivity extends BaseActivity {
private EditText login_uid;
private EditText login_passwd;
boolean isLoadingInstance = false;
private String oldSearch;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -141,6 +143,12 @@ public class LoginActivity extends BaseActivity {
if (theme == Helper.THEME_LIGHT) {
connectionButton.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.white));
}
login_instance.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick (AdapterView<?> parent, View view, int position, long id) {
oldSearch = parent.getItemAtPosition(position).toString().trim();
}
});
login_instance.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
@ -157,9 +165,10 @@ public class LoginActivity extends BaseActivity {
final String action = "/instances/search";
final HashMap<String, String> parameters = new HashMap<>();
parameters.put("q", s.toString().trim());
parameters.put("count", String.valueOf(5));
parameters.put("count", String.valueOf(50));
parameters.put("name", String.valueOf(true));
isLoadingInstance = true;
if( oldSearch == null || !oldSearch.equals(s.toString().trim()))
new Thread(new Runnable() {
@Override
public void run() {
@ -173,9 +182,18 @@ public class LoginActivity extends BaseActivity {
JSONObject jsonObject = new JSONObject(response);
JSONArray jsonArray = jsonObject.getJSONArray("instances");
if (jsonArray != null) {
instances = new String[jsonArray.length()];
int length = 0;
for (int i = 0; i < jsonArray.length(); i++) {
instances[i] = jsonArray.getJSONObject(i).get("name").toString();
if( !jsonArray.getJSONObject(i).get("name").toString().contains("@"))
length++;
}
instances = new String[length];
int j = 0;
for (int i = 0; i < jsonArray.length(); i++) {
if( !jsonArray.getJSONObject(i).get("name").toString().contains("@")) {
instances[j] = jsonArray.getJSONObject(i).get("name").toString();
j++;
}
}
} else {
instances = new String[]{};
@ -186,6 +204,7 @@ public class LoginActivity extends BaseActivity {
login_instance.setAdapter(adapter);
if (login_instance.hasFocus() && !LoginActivity.this.isFinishing())
login_instance.showDropDown();
oldSearch = s.toString().trim();
} catch (JSONException ignored) {
isLoadingInstance = false;

View File

@ -47,6 +47,7 @@ public class RetrieveFeedsAsyncTask extends AsyncTask<Void, Void, Void> {
private boolean showPinned = false;
private WeakReference<Context> contextReference;
private FilterToots filterToots;
private String instanceName;
public enum Type{
HOME,
@ -60,7 +61,8 @@ public class RetrieveFeedsAsyncTask extends AsyncTask<Void, Void, Void> {
CONTEXT,
TAG,
CACHE_BOOKMARKS,
CACHE_STATUS
CACHE_STATUS,
REMOTE_INSTANCE
}
@ -79,6 +81,14 @@ public class RetrieveFeedsAsyncTask extends AsyncTask<Void, Void, Void> {
this.listener = onRetrieveFeedsInterface;
}
public RetrieveFeedsAsyncTask(Context context, Type action, String instanceName, String max_id, OnRetrieveFeedsInterface onRetrieveFeedsInterface){
this.contextReference = new WeakReference<>(context);
this.action = action;
this.max_id = max_id;
this.listener = onRetrieveFeedsInterface;
this.instanceName = instanceName;
}
public RetrieveFeedsAsyncTask(Context context, Type action, String targetedID, String max_id, boolean showMediaOnly, boolean showPinned, OnRetrieveFeedsInterface onRetrieveFeedsInterface){
this.contextReference = new WeakReference<>(context);
this.action = action;
@ -111,6 +121,9 @@ public class RetrieveFeedsAsyncTask extends AsyncTask<Void, Void, Void> {
case PUBLIC:
apiResponse = api.getPublicTimeline(false, max_id);
break;
case REMOTE_INSTANCE:
apiResponse = api.getPublicTimeline(this.instanceName,false, max_id);
break;
case FAVOURITES:
apiResponse = api.getFavourites(max_id);
break;

View File

@ -16,6 +16,7 @@ package fr.gouv.etalab.mastodon.client;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
@ -530,6 +531,16 @@ public class API {
}
/**
* Retrieves public timeline for the account *synchronously*
* @param local boolean only local timeline
* @param max_id String id max
* @return APIResponse
*/
public APIResponse getPublicTimeline(String instanceName, boolean local, String max_id){
return getPublicTimeline(local, instanceName, max_id, null, tootPerPage);
}
/**
* Retrieves public timeline for the account *synchronously*
* @param local boolean only local timeline
@ -537,7 +548,7 @@ public class API {
* @return APIResponse
*/
public APIResponse getPublicTimeline(boolean local, String max_id){
return getPublicTimeline(local, max_id, null, tootPerPage);
return getPublicTimeline(local, null, max_id, null, tootPerPage);
}
/**
@ -547,7 +558,7 @@ public class API {
* @return APIResponse
*/
public APIResponse getPublicTimelineSinceId(boolean local, String since_id) {
return getPublicTimeline(local, null, since_id, tootPerPage);
return getPublicTimeline(local, null, null, since_id, tootPerPage);
}
@ -560,7 +571,7 @@ public class API {
* @param limit int limit - max value 40
* @return APIResponse
*/
private APIResponse getPublicTimeline(boolean local, String max_id, String since_id, int limit){
private APIResponse getPublicTimeline(boolean local, String instanceName, String max_id, String since_id, int limit){
HashMap<String, String> params = new HashMap<>();
if( local)
@ -575,7 +586,12 @@ public class API {
statuses = new ArrayList<>();
try {
HttpsConnection httpsConnection = new HttpsConnection(context);
String response = httpsConnection.get(getAbsoluteUrl("/timelines/public"), 60, params, prefKeyOauthTokenT);
String url;
if( instanceName == null)
url = getAbsoluteUrl("/timelines/public");
else
url = getAbsoluteUrlRemoteInstance(instanceName);
String response = httpsConnection.get(url, 60, params, prefKeyOauthTokenT);
apiResponse.setSince_id(httpsConnection.getSince_id());
apiResponse.setMax_id(httpsConnection.getMax_id());
statuses = parseStatuses(new JSONArray(response));
@ -2291,4 +2307,7 @@ public class API {
}
private String getAbsoluteUrlRemoteInstance(String instanceName) {
return "https://" + instanceName + "/api/v1/timelines/public?local=true";
}
}

View File

@ -81,6 +81,7 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
private String userId, instance;
private SharedPreferences sharedpreferences;
private boolean isSwipped;
private String remoteInstance;
public DisplayStatusFragment(){
}
@ -101,6 +102,7 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
tag = bundle.getString("tag", null);
showMediaOnly = bundle.getBoolean("showMediaOnly",false);
showPinned = bundle.getBoolean("showPinned",false);
remoteInstance = bundle.getString("remote_instance", "");
}
isSwipped = false;
max_id = null;
@ -151,6 +153,8 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
asyncTask = new RetrieveFeedsAsyncTask(context, type, targetedId, max_id, showMediaOnly, showPinned, DisplayStatusFragment.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else if( type == RetrieveFeedsAsyncTask.Type.TAG)
asyncTask = new RetrieveFeedsAsyncTask(context, type, tag, targetedId, max_id, DisplayStatusFragment.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else if( remoteInstance != null)
asyncTask = new RetrieveFeedsAsyncTask(context, type, remoteInstance, max_id, DisplayStatusFragment.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else
asyncTask = new RetrieveFeedsAsyncTask(context, type, max_id, DisplayStatusFragment.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
@ -208,6 +212,8 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
asyncTask = new RetrieveFeedsAsyncTask(context, type, targetedId, max_id, showMediaOnly, showPinned, DisplayStatusFragment.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else if (type == RetrieveFeedsAsyncTask.Type.TAG)
asyncTask = new RetrieveFeedsAsyncTask(context, type, tag, targetedId, max_id, DisplayStatusFragment.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else if( remoteInstance != null)
asyncTask = new RetrieveFeedsAsyncTask(context, type, remoteInstance, max_id, DisplayStatusFragment.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else {
if( type == RetrieveFeedsAsyncTask.Type.HOME ){
String bookmark;
@ -230,6 +236,8 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
asyncTask = new RetrieveFeedsAsyncTask(context, type, targetedId, max_id, showMediaOnly, showPinned, DisplayStatusFragment.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else if (type == RetrieveFeedsAsyncTask.Type.TAG)
asyncTask = new RetrieveFeedsAsyncTask(context, type, tag, targetedId, max_id, DisplayStatusFragment.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else if( remoteInstance != null)
asyncTask = new RetrieveFeedsAsyncTask(context, type, remoteInstance, max_id, DisplayStatusFragment.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else {
if( type == RetrieveFeedsAsyncTask.Type.HOME ){
String bookmark;
@ -397,7 +405,7 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
}
} else if (type == RetrieveFeedsAsyncTask.Type.PUBLIC || type == RetrieveFeedsAsyncTask.Type.LOCAL) {
} else if (type == RetrieveFeedsAsyncTask.Type.PUBLIC || type == RetrieveFeedsAsyncTask.Type.REMOTE_INSTANCE) {
status.setReplies(new ArrayList<Status>());
status.setNew(false);

View File

@ -142,6 +142,7 @@ import fr.gouv.etalab.mastodon.BuildConfig;
import fr.gouv.etalab.mastodon.R;
import fr.gouv.etalab.mastodon.activities.BaseMainActivity;
import fr.gouv.etalab.mastodon.activities.HashTagActivity;
import fr.gouv.etalab.mastodon.activities.InstanceFederatedActivity;
import fr.gouv.etalab.mastodon.activities.LoginActivity;
import fr.gouv.etalab.mastodon.activities.MainActivity;
import fr.gouv.etalab.mastodon.activities.ShowAccountActivity;
@ -158,6 +159,7 @@ import fr.gouv.etalab.mastodon.client.Entities.Status;
import fr.gouv.etalab.mastodon.client.Entities.Tag;
import fr.gouv.etalab.mastodon.client.Entities.Version;
import fr.gouv.etalab.mastodon.sqlite.AccountDAO;
import fr.gouv.etalab.mastodon.sqlite.InstancesDAO;
import fr.gouv.etalab.mastodon.sqlite.SearchDAO;
import fr.gouv.etalab.mastodon.sqlite.Sqlite;
@ -213,6 +215,7 @@ public class Helper {
public static final String SHOULD_CONTINUE_STREAMING_LOCAL = "should_continue_streaming_local";
public static final String SEARCH_KEYWORD = "search_keyword";
public static final String CLIP_BOARD = "clipboard";
public static final String INSTANCE_NAME = "instance_name";
//Notifications
public static final int NOTIFICATION_INTENT = 1;
public static final int HOME_TIMELINE_INTENT = 2;
@ -221,6 +224,8 @@ public class Helper {
public static final int ADD_USER_INTENT = 5;
public static final int BACKUP_INTENT = 6;
public static final int SEARCH_TAG = 7;
public static final int SEARCH_INSTANCE = 8;
//Settings
public static final String SET_TOOTS_PER_PAGE = "set_toots_per_page";
public static final String SET_ACCOUNTS_PER_PAGE = "set_accounts_per_page";
@ -2162,6 +2167,54 @@ public class Helper {
}
public static void addInstanceTab(Context context, TabLayout tableLayout, InstanceFederatedActivity.PagerAdapter pagerAdapter){
SQLiteDatabase db = Sqlite.getInstance(context, Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
new InstancesDAO(context, db).cleanDoublon();
List<String> instances = new InstancesDAO(context, db).getAllInstances();
int allTabCount = tableLayout.getTabCount();
while(allTabCount > 0){
removeTab(tableLayout, pagerAdapter, allTabCount-1);
allTabCount -=1;
}
if( instances != null) {
for (String instance : instances) {
addTab(tableLayout, pagerAdapter, instance);
}
if( instances.size() > 0 ){
tableLayout.setTabGravity(TabLayout.GRAVITY_FILL);
tableLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
}
}
}
public static void removeSearchTab(String keyword, TabLayout tableLayout, InstanceFederatedActivity.PagerAdapter pagerAdapter){
int selection = -1;
for(int i = 0; i < tableLayout.getTabCount() ; i++ ){
if( tableLayout.getTabAt(i).getText() != null && tableLayout.getTabAt(i).getText().equals(keyword)) {
selection = i;
break;
}
}
if( selection != -1)
removeTab(tableLayout, pagerAdapter, selection);
}
private static void removeTab(TabLayout tableLayout, InstanceFederatedActivity.PagerAdapter pagerAdapter, int position) {
if (tableLayout.getTabCount() >= position) {
tableLayout.removeTabAt(position);
pagerAdapter.removeTabPage();
}
}
private static void addTab(TabLayout tableLayout, InstanceFederatedActivity.PagerAdapter pagerAdapter, String title) {
tableLayout.addTab(tableLayout.newTab().setText(title));
pagerAdapter.addTabPage(title);
}
public static void addSearchTag(Context context, TabLayout tableLayout, BaseMainActivity.PagerAdapter pagerAdapter){
SQLiteDatabase db = Sqlite.getInstance(context, Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
List<String> searches = new SearchDAO(context, db).getAllSearch();

View File

@ -0,0 +1,140 @@
package fr.gouv.etalab.mastodon.sqlite;
/* Copyright 2018 Thomas Schneider
*
* This file is a part of Mastalab
*
* 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.
*
* Mastalab 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 Mastalab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import fr.gouv.etalab.mastodon.helper.Helper;
/**
* Created by Thomas on 20/08/2018.
* Manage instance names in DB
*/
public class InstancesDAO {
private SQLiteDatabase db;
public Context context;
private String userId;
public InstancesDAO(Context context, SQLiteDatabase db) {
//Creation of the DB with tables
this.context = context;
this.db = db;
SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE);
userId = sharedpreferences.getString(Helper.PREF_KEY_ID, null);
}
//------- INSERTIONS -------
/**
* Insert an instance name in database
* @param instanceName String
*/
public void insertInstance(String instanceName) {
ContentValues values = new ContentValues();
values.put(Sqlite.COL_INSTANCE, instanceName);
values.put(Sqlite.COL_USER_ID, userId);
values.put(Sqlite.COL_DATE_CREATION, Helper.dateToString(new Date()));
//Inserts search
try{
db.insert(Sqlite.TABLE_INSTANCES, null, values);
}catch (Exception ignored) {}
}
//------- REMOVE -------
/***
* Remove instance by its name
* @return int
*/
public int remove(String instanceName){
return db.delete(Sqlite.TABLE_INSTANCES, Sqlite.COL_INSTANCE + " = \"" + instanceName + "\" AND " + Sqlite.COL_USER_ID + " = \"" + userId+ "\"", null);
}
//------- REMOVE -------
/***
* Remove instance by its name
* @return int
*/
public int cleanDoublon(){
return db.delete(Sqlite.TABLE_INSTANCES, Sqlite.COL_ID + " NOT IN (" +
" SELECT MIN("+Sqlite.COL_ID+")" +
" FROM " + Sqlite.TABLE_INSTANCES +
" GROUP BY "+ Sqlite.COL_INSTANCE + "," + Sqlite.COL_USER_ID +")", null);
}
//------- GETTERS -------
/**
* Returns all instances in db for a user
* @return instances List<String>
*/
public List<String> getAllInstances(){
try {
Cursor c = db.query(Sqlite.TABLE_INSTANCES, null, Sqlite.COL_USER_ID + " = '" + userId+ "'", null, null, null, Sqlite.COL_INSTANCE + " ASC", null);
return cursorToListSearch(c);
} catch (Exception e) {
return null;
}
}
/**
* Returns instance by its nale in db
* @return instance List<String>
*/
public List<String> getInstanceByName(String keyword){
try {
Cursor c = db.query(Sqlite.TABLE_INSTANCES, null, Sqlite.COL_INSTANCE + " = \"" + keyword + "\" AND " + Sqlite.COL_USER_ID + " = \"" + userId+ "\"", null, null, null, null, null);
return cursorToListSearch(c);
} catch (Exception e) {
return null;
}
}
/***
* Method to hydrate stored instances from database
* @param c Cursor
* @return List<String>
*/
private List<String> cursorToListSearch(Cursor c){
//No element found
if (c.getCount() == 0)
return null;
List<String> instances = new ArrayList<>();
while (c.moveToNext() ) {
instances.add(c.getString(c.getColumnIndex(Sqlite.COL_INSTANCE)));
}
//Close the cursor
c.close();
//Search list is returned
return instances;
}
}

View File

@ -26,7 +26,7 @@ import android.database.sqlite.SQLiteOpenHelper;
public class Sqlite extends SQLiteOpenHelper {
public static final int DB_VERSION = 11;
public static final int DB_VERSION = 12;
public static final String DB_NAME = "mastodon_etalab_db";
public static SQLiteDatabase db;
private static Sqlite sInstance;
@ -49,6 +49,8 @@ public class Sqlite extends SQLiteOpenHelper {
//Table for cached statuses
static final String TABLE_STATUSES_CACHE = "STATUSES_CACHE";
//Table for instance names
static final String TABLE_INSTANCES = "INSTANCES";
static final String COL_USER_ID = "USER_ID";
static final String COL_USERNAME = "USERNAME";
@ -159,6 +161,11 @@ public class Sqlite extends SQLiteOpenHelper {
+ TABLE_STATUSES_CACHE + "(" + COL_INSTANCE +"," + COL_STATUS_ID + ")";
private final String CREATE_TABLE_INSTANCES = "CREATE TABLE " + TABLE_INSTANCES + " ("
+ COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ COL_INSTANCE + " TEXT NOT NULL, " + COL_USER_ID + " TEXT NOT NULL, " + COL_DATE_CREATION + " TEXT NOT NULL)";
public Sqlite(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
@ -181,6 +188,7 @@ public class Sqlite extends SQLiteOpenHelper {
db.execSQL(CREATE_TABLE_TEMP_MUTE);
db.execSQL(CREATE_TABLE_STATUSES_CACHE);
db.execSQL(CREATE_UNIQUE_CACHE_INDEX);
db.execSQL(CREATE_TABLE_INSTANCES);
}
@Override
@ -214,6 +222,8 @@ public class Sqlite extends SQLiteOpenHelper {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_STATUSES_CACHE);
db.execSQL(CREATE_TABLE_STATUSES_CACHE);
db.execSQL(CREATE_UNIQUE_CACHE_INDEX);
case 11:
db.execSQL(CREATE_TABLE_INSTANCES);
default:
break;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2018 Thomas Schneider
This file is a part of Mastalab
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.
Mastalab 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 Mastalab; if not,
see <http://www.gnu.org/licenses>
-->
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="fr.gouv.etalab.mastodon.activities.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay"
>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
android:theme="@style/AppThemeDark_NoActionBar"
app:popupTheme="?attr/popupOverlay">
<ImageView
android:id="@+id/iconbar"
android:layout_width="30dp"
android:layout_height="30dp"
tools:ignore="ContentDescription" />
<LinearLayout
android:id="@+id/toolbar_search_container"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal">
<android.support.design.widget.TabLayout
android:id="@+id/tabLayout"
android:layout_width="0dp"
android:layout_weight="1"
app:tabGravity="fill"
app:tabMaxWidth="0dp"
app:tabIndicatorHeight="0dp"
android:layout_height="wrap_content"
app:tabSelectedTextColor="?attr/colorAccent"
/>
</LinearLayout>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<!-- Framelayout to display Fragments -->
<android.support.v4.view.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/viewpager"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="fr.gouv.etalab.mastodon.activities.MainActivity"
>
</android.support.v4.view.ViewPager>
<android.support.design.widget.FloatingActionButton
android:id="@+id/add_new_instance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin_floating"
app:srcCompat="@drawable/ic_action_add_new"
tools:ignore="VectorDrawableCompat" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/federated_timeline_close"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="bottom|start"
app:fabSize="mini"
app:srcCompat="@drawable/ic_close"
android:layout_margin="@dimen/fab_margin_floating"
tools:ignore="VectorDrawableCompat" />
</android.support.design.widget.CoordinatorLayout>
</android.support.v4.widget.DrawerLayout>

View File

@ -136,6 +136,16 @@
android:layout_margin="@dimen/fab_margin_floating"
app:srcCompat="@drawable/ic_action_add_new"
tools:ignore="VectorDrawableCompat" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/federated_timeline"
android:layout_width="40dp"
android:layout_height="40dp"
app:fabSize="mini"
android:layout_gravity="bottom|start"
android:layout_margin="@dimen/fab_margin_floating"
app:srcCompat="@drawable/ic_public_world"
tools:ignore="VectorDrawableCompat" />
</android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.NavigationView

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content">
<AutoCompleteTextView
android:id="@+id/search_instance"
android:inputType="text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/instance"
android:maxLines="1"
/>
</LinearLayout>

View File

@ -544,6 +544,7 @@
<string name="support_the_app_on_liberapay">Support the app on Liberapay</string>
<string name="alert_regex">There is an error in the regular expression!</string>
<string name="no_account_yet">No account yet?</string>
<string name="toast_instance_unavailable">No timelines was found on this instance!</string>
<string-array translatable="false" name="proxy_type_choice">
<item>HTTP</item>
<item>SOCKS</item>