diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2e9596059..99898a6d1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -63,6 +63,12 @@ android:name=".services.StreamingLocalTimelineService" android:exported="false"/> + . */ +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 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 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 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); + } + + } + +} \ No newline at end of file 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 dde695adb..adf90c010 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 @@ -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 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; 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 072234d26..7cacba924 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 @@ -47,6 +47,7 @@ public class RetrieveFeedsAsyncTask extends AsyncTask { private boolean showPinned = false; private WeakReference contextReference; private FilterToots filterToots; + private String instanceName; public enum Type{ HOME, @@ -60,7 +61,8 @@ public class RetrieveFeedsAsyncTask extends AsyncTask { CONTEXT, TAG, CACHE_BOOKMARKS, - CACHE_STATUS + CACHE_STATUS, + REMOTE_INSTANCE } @@ -79,6 +81,14 @@ public class RetrieveFeedsAsyncTask extends AsyncTask { 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 { 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; 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 49a5daaef..edce6d3d4 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,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 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"; + } } 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 ebcf19d65..c8589e80e 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 @@ -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.setNew(false); 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 a6359cb72..c1ada3908 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 @@ -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 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 searches = new SearchDAO(context, db).getAllSearch(); diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/sqlite/InstancesDAO.java b/app/src/main/java/fr/gouv/etalab/mastodon/sqlite/InstancesDAO.java new file mode 100644 index 000000000..87c607bbb --- /dev/null +++ b/app/src/main/java/fr/gouv/etalab/mastodon/sqlite/InstancesDAO.java @@ -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 . */ + +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 + */ + public List 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 + */ + public List 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 + */ + private List cursorToListSearch(Cursor c){ + //No element found + if (c.getCount() == 0) + return null; + List 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; + } +} diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/sqlite/Sqlite.java b/app/src/main/java/fr/gouv/etalab/mastodon/sqlite/Sqlite.java index e1fc26b30..4f5d0dbd8 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/sqlite/Sqlite.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/sqlite/Sqlite.java @@ -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; } diff --git a/app/src/main/res/drawable-hdpi/ic_public_world.png b/app/src/main/res/drawable-hdpi/ic_public_world.png new file mode 100644 index 000000000..d315c13e6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_public_world.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_public_world.png b/app/src/main/res/drawable-ldpi/ic_public_world.png new file mode 100644 index 000000000..12e648027 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_public_world.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_public_world.png b/app/src/main/res/drawable-mdpi/ic_public_world.png new file mode 100644 index 000000000..1056867c2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_public_world.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_public_world.png b/app/src/main/res/drawable-xhdpi/ic_public_world.png new file mode 100644 index 000000000..348ca0b97 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_public_world.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_public_world.png b/app/src/main/res/drawable-xxhdpi/ic_public_world.png new file mode 100644 index 000000000..a3ebc0a68 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_public_world.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_public_world.png b/app/src/main/res/drawable-xxxhdpi/ic_public_world.png new file mode 100644 index 000000000..c138ca075 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_public_world.png differ diff --git a/app/src/main/res/layout/activity_federated.xml b/app/src/main/res/layout/activity_federated.xml new file mode 100644 index 000000000..ff7949932 --- /dev/null +++ b/app/src/main/res/layout/activity_federated.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ac41b9c32..cd42290b9 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -136,6 +136,16 @@ android:layout_margin="@dimen/fab_margin_floating" app:srcCompat="@drawable/ic_action_add_new" tools:ignore="VectorDrawableCompat" /> + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 80d10cf8d..9759a4c93 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -544,6 +544,7 @@ Support the app on Liberapay There is an error in the regular expression! No account yet? + No timelines was found on this instance! HTTP SOCKS