diff --git a/android/TriblerApp/app/src/main/AndroidManifest.xml b/android/TriblerApp/app/src/main/AndroidManifest.xml index 03c9fd24abd..aa8e10b2ba7 100644 --- a/android/TriblerApp/app/src/main/AndroidManifest.xml +++ b/android/TriblerApp/app/src/main/AndroidManifest.xml @@ -100,11 +100,16 @@ + \ No newline at end of file diff --git a/android/TriblerApp/app/src/main/java/org/tribler/android/ChannelActivity.java b/android/TriblerApp/app/src/main/java/org/tribler/android/ChannelActivity.java index 3978c203134..2c2c68d79a1 100644 --- a/android/TriblerApp/app/src/main/java/org/tribler/android/ChannelActivity.java +++ b/android/TriblerApp/app/src/main/java/org/tribler/android/ChannelActivity.java @@ -16,9 +16,12 @@ public class ChannelActivity extends BaseActivity { - public static final String ACTION_TOGGLE_SUBSCRIBED = "org.tribler.android.TOGGLE_SUBSCRIBED"; - public static final String EXTRA_SUBSCRIBED = "org.tribler.android.SUBSCRIBED"; - public static final String EXTRA_DISPERSY_CID = "org.tribler.android.dispersy.CID"; + public static final String ACTION_TOGGLE_SUBSCRIBED = "org.tribler.android.channel.TOGGLE_SUBSCRIBED"; + + public static final String EXTRA_DISPERSY_CID = "org.tribler.android.channel.dispersy.CID"; + public static final String EXTRA_NAME = "org.tribler.android.channel.NAME"; + public static final String EXTRA_DESCRIPTION = "org.tribler.android.channel.DESCRIPTION"; + public static final String EXTRA_SUBSCRIBED = "org.tribler.android.channel.SUBSCRIBED"; private ChannelFragment _fragment; @@ -106,9 +109,9 @@ protected void handleIntent(Intent intent) { if (TextUtils.isEmpty(action)) { return; } - String title = intent.getStringExtra(Intent.EXTRA_TITLE); - String dispersyCid = intent.getStringExtra(ChannelActivity.EXTRA_DISPERSY_CID); - boolean subscribed = intent.getBooleanExtra(ChannelActivity.EXTRA_SUBSCRIBED, false); + String dispersyCid = intent.getStringExtra(EXTRA_DISPERSY_CID); + String name = intent.getStringExtra(EXTRA_NAME); + boolean subscribed = intent.getBooleanExtra(EXTRA_SUBSCRIBED, false); switch (action) { @@ -117,20 +120,20 @@ protected void handleIntent(Intent intent) { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { if (!subscribed) { - title = getString(R.string.title_channel_preview) + ": " + title; + name = getString(R.string.title_channel_preview) + ": " + name; } - actionBar.setTitle(title); + actionBar.setTitle(name); } return; case ACTION_TOGGLE_SUBSCRIBED: if (subscribed) { - _fragment.unsubscribe(dispersyCid, subscribed, title); + _fragment.unsubscribe(dispersyCid, subscribed, name); } else { - _fragment.subscribe(dispersyCid, subscribed, title); + _fragment.subscribe(dispersyCid, subscribed, name); } // Update view - intent.putExtra(ChannelActivity.EXTRA_SUBSCRIBED, !subscribed); + intent.putExtra(EXTRA_SUBSCRIBED, !subscribed); invalidateOptionsMenu(); // Flag modification diff --git a/android/TriblerApp/app/src/main/java/org/tribler/android/DefaultInteractionListFragment.java b/android/TriblerApp/app/src/main/java/org/tribler/android/DefaultInteractionListFragment.java index 1035462c236..54e82ce55b5 100644 --- a/android/TriblerApp/app/src/main/java/org/tribler/android/DefaultInteractionListFragment.java +++ b/android/TriblerApp/app/src/main/java/org/tribler/android/DefaultInteractionListFragment.java @@ -73,8 +73,8 @@ public void onDetach() { public void onClick(final TriblerChannel channel) { Intent intent = new Intent(_context, ChannelActivity.class); intent.setAction(Intent.ACTION_GET_CONTENT); - intent.putExtra(Intent.EXTRA_TITLE, channel.getName()); intent.putExtra(ChannelActivity.EXTRA_DISPERSY_CID, channel.getDispersyCid()); + intent.putExtra(ChannelActivity.EXTRA_NAME, channel.getName()); intent.putExtra(ChannelActivity.EXTRA_SUBSCRIBED, channel.isSubscribed()); startActivityForResult(intent, CHANNEL_ACTIVITY_REQUEST_CODE); } @@ -84,18 +84,25 @@ public void onClick(final TriblerChannel channel) { */ @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == CHANNEL_ACTIVITY_REQUEST_CODE) { - if (resultCode == Activity.RESULT_FIRST_USER) { - // Update the subscription status of the channel identified by dispersy_cid - String dispersyCid = data.getStringExtra(ChannelActivity.EXTRA_DISPERSY_CID); - boolean subscribed = data.getBooleanExtra(ChannelActivity.EXTRA_SUBSCRIBED, false); - TriblerChannel channel = adapter.findByDispersyCid(dispersyCid); - if (channel != null) { - channel.setSubscribed(subscribed); - // Update view - adapter.notifyObjectChanged(channel); + switch (requestCode) { + + case CHANNEL_ACTIVITY_REQUEST_CODE: + switch (resultCode) { + + case Activity.RESULT_FIRST_USER: + // Update the subscription status of the channel identified by dispersy_cid + String dispersyCid = data.getStringExtra(ChannelActivity.EXTRA_DISPERSY_CID); + boolean subscribed = data.getBooleanExtra(ChannelActivity.EXTRA_SUBSCRIBED, false); + + TriblerChannel channel = adapter.findByDispersyCid(dispersyCid); + if (channel != null) { + channel.setSubscribed(subscribed); + // Update view + adapter.notifyObjectChanged(channel); + } + break; } - } + break; } } diff --git a/android/TriblerApp/app/src/main/java/org/tribler/android/EditChannelActivity.java b/android/TriblerApp/app/src/main/java/org/tribler/android/EditChannelActivity.java new file mode 100644 index 00000000000..7bd848c1394 --- /dev/null +++ b/android/TriblerApp/app/src/main/java/org/tribler/android/EditChannelActivity.java @@ -0,0 +1,167 @@ +package org.tribler.android; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; + +import org.tribler.android.restapi.IRestApi; +import org.tribler.android.restapi.TriblerService; +import org.tribler.android.restapi.json.AddedChannelAck; +import org.tribler.android.restapi.json.ModifiedAck; + +import butterknife.BindView; +import rx.Observer; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; + +public class EditChannelActivity extends BaseActivity { + + public static final String ACTION_CREATE_CHANNEL = "org.tribler.android.channel.CREATE"; + + private IRestApi _service; + + @BindView(R.id.channel_icon_wrapper) + View iconWrapper; + + @BindView(R.id.channel_icon) + ImageView icon; + + @BindView(R.id.channel_capital) + TextView nameCapital; + + @BindView(R.id.my_channel_explanation) + TextView explanation; + + @BindView(R.id.channel_name_input) + EditText nameInput; + + @BindView(R.id.channel_description_input) + EditText descriptionInput; + + @BindView(R.id.btn_channel_save) + Button btnSave; + + @BindView(R.id.channel_progress) + View progressView; + + @BindView(R.id.channel_progress_status) + TextView statusBar; + + /** + * {@inheritDoc} + */ + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_edit_channel); + + String baseUrl = getString(R.string.service_url) + ":" + getString(R.string.service_port_number); + String authToken = getString(R.string.service_auth_token); + _service = TriblerService.createService(baseUrl, authToken); + + handleIntent(getIntent()); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + _service = null; + } + + /** + * {@inheritDoc} + */ + @Override + protected void handleIntent(Intent intent) { + String action = intent.getAction(); + if (TextUtils.isEmpty(action)) { + return; + } + switch (action) { + + case ACTION_CREATE_CHANNEL: + btnSave.setText(getText(R.string.action_create)); + explanation.setVisibility(View.VISIBLE); + return; + + case Intent.ACTION_EDIT: + String dispersyCid = intent.getStringExtra(ChannelActivity.EXTRA_DISPERSY_CID); + String name = intent.getStringExtra(ChannelActivity.EXTRA_NAME); + String description = intent.getStringExtra(ChannelActivity.EXTRA_DESCRIPTION); + int color = MyUtils.getColor(dispersyCid.hashCode()); + nameCapital.setText(MyUtils.getCapitals(name, 2)); + nameInput.setText(name); + descriptionInput.setText(description); + MyUtils.setCicleBackground(icon, color); + iconWrapper.setVisibility(View.VISIBLE); + return; + } + } + + public void btnChannelSaveClicked(@Nullable View view) { + // Lock input fields + btnSave.setEnabled(false); + nameInput.setEnabled(false); + descriptionInput.setEnabled(false); + + // Show loading indicator + progressView.setVisibility(View.VISIBLE); + btnSave.setVisibility(View.GONE); + + String name = nameInput.getText().toString(); + String description = descriptionInput.getText().toString(); + + if (ACTION_CREATE_CHANNEL.equals(getIntent().getAction())) { + statusBar.setText(getText(R.string.status_creating_channel)); + + rxSubs.add(_service.createChannel(name, description) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + + public void onNext(AddedChannelAck ack) { + } + + public void onCompleted() { + setResult(Activity.RESULT_OK); + finish(); + } + + public void onError(Throwable e) { + Log.e("btnChannelSaveClicked", "createChannel", e); + // Retry + btnChannelSaveClicked(null); + } + })); + } else { + + rxSubs.add(_service.editMyChannel(name, description) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + + public void onNext(ModifiedAck ack) { + } + + public void onCompleted() { + setResult(Activity.RESULT_OK); + finish(); + } + + public void onError(Throwable e) { + Log.e("btnChannelSaveClicked", "editMyChannel", e); + // Retry + btnChannelSaveClicked(null); + } + })); + } + } +} diff --git a/android/TriblerApp/app/src/main/java/org/tribler/android/MyChannelFragment.java b/android/TriblerApp/app/src/main/java/org/tribler/android/MyChannelFragment.java index f50b8573923..c5bf735a9db 100644 --- a/android/TriblerApp/app/src/main/java/org/tribler/android/MyChannelFragment.java +++ b/android/TriblerApp/app/src/main/java/org/tribler/android/MyChannelFragment.java @@ -1,6 +1,10 @@ package org.tribler.android; +import android.app.Activity; +import android.content.Intent; import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.app.ActionBar; import android.support.v7.widget.SearchView; import android.util.Log; import android.view.Menu; @@ -11,9 +15,11 @@ import com.jakewharton.rxbinding.support.v7.widget.RxSearchView; import com.jakewharton.rxbinding.support.v7.widget.SearchViewQueryTextEvent; -import org.tribler.android.restapi.json.TriblerChannel; +import org.tribler.android.restapi.json.ChannelOverviewPart; +import org.tribler.android.restapi.json.MyChannelResponse; import org.tribler.android.restapi.json.TriblerTorrent; +import retrofit2.adapter.rxjava.HttpException; import rx.Observable; import rx.Observer; import rx.android.schedulers.AndroidSchedulers; @@ -21,6 +27,13 @@ public class MyChannelFragment extends DefaultInteractionListFragment { + public static final String ACTION_CREATE_CHANNEL = "org.tribler.android.channel.CREATE"; + + public static final int CREATE_CHANNEL_ACTIVITY_REQUEST_CODE = 401; + public static final int EDIT_CHANNEL_ACTIVITY_REQUEST_CODE = 402; + + private ChannelOverviewPart _overview; + /** * {@inheritDoc} */ @@ -31,6 +44,18 @@ public void onCreate(Bundle savedInstanceState) { loadMyChannel(); } + /** + * {@inheritDoc} + */ + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + // Is my channel created? + boolean created = _overview != null; + + //TODO show error state + } + /** * {@inheritDoc} */ @@ -71,6 +96,19 @@ public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); // Hide main search button menu.findItem(R.id.btn_search).setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_NEVER); + + // Is my channel created? + boolean created = _overview != null; + menu.findItem(R.id.btn_add_my_channel).setEnabled(created); + menu.findItem(R.id.btn_filter_my_channel).setEnabled(created); + + // Set title + if (created) { + ActionBar actionBar = ((BaseActivity) getActivity()).getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(_overview.getName()); + } + } } /** @@ -103,14 +141,23 @@ public void reload() { } private void loadMyChannel() { - loading = service.getPopularChannels(5) + loading = service.getMyChannel() .subscribeOn(Schedulers.io()) - .flatMap(response -> Observable.from(response.getChannels())) + .map(MyChannelResponse::getOverview) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { + .doOnNext(overview -> { + // Side effects: + _overview = overview; + if (isAdded()) { + getActivity().invalidateOptionsMenu(); + } + }) + .switchMap(overview -> service.getTorrents(_overview.getIdentifier())) + .flatMap(response -> Observable.from(response.getTorrents())) + .subscribe(new Observer() { - public void onNext(TriblerChannel channel) { - adapter.addObject(channel); + public void onNext(TriblerTorrent torrent) { + adapter.addObject(torrent); } public void onCompleted() { @@ -119,11 +166,43 @@ public void onCompleted() { } public void onError(Throwable e) { - Log.e("loadMyChannel", "getChannels", e); - // Retry - reload(); + if (e instanceof HttpException && ((HttpException) e).code() == 404) { + // My channel has not been created yet + Intent createIntent = MyUtils.createChannel(); + startActivityForResult(createIntent, CREATE_CHANNEL_ACTIVITY_REQUEST_CODE); + } else { + Log.e("loadMyChannel", "getOverview", e); + // Retry + reload(); + } } }); rxSubs.add(loading); } + + /** + * {@inheritDoc} + */ + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + + case CREATE_CHANNEL_ACTIVITY_REQUEST_CODE: + switch (resultCode) { + + case Activity.RESULT_OK: + loadMyChannel(); + break; + + case Activity.RESULT_CANCELED: + // Hide loading indicator + progressView.setVisibility(View.GONE); + + //TODO: show error message + break; + } + break; + } + } + } diff --git a/android/TriblerApp/app/src/main/java/org/tribler/android/MyUtils.java b/android/TriblerApp/app/src/main/java/org/tribler/android/MyUtils.java index e57784e0853..8e92949dccc 100644 --- a/android/TriblerApp/app/src/main/java/org/tribler/android/MyUtils.java +++ b/android/TriblerApp/app/src/main/java/org/tribler/android/MyUtils.java @@ -5,12 +5,15 @@ import android.content.Intent; import android.content.res.Configuration; import android.graphics.Color; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.OvalShape; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Environment; import android.provider.MediaStore; import android.webkit.MimeTypeMap; +import android.widget.ImageView; import com.squareup.leakcanary.LeakCanary; import com.squareup.leakcanary.RefWatcher; @@ -19,6 +22,7 @@ import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.Random; import okio.BufferedSource; import rx.Observable; @@ -32,6 +36,8 @@ public class MyUtils { private MyUtils() { } + private static Random rand = new Random(); + private static RefWatcher _refWatcher; public static RefWatcher getRefWatcher(Context ctx) { @@ -161,13 +167,40 @@ public static String humanReadableByteCount(long bytes, boolean si) { return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); } - public static int getColor(int hashCode) throws Exception { + public static int getColor(int hashCode) { int r = (hashCode & 0xFF0000) >> 16; int g = (hashCode & 0x00FF00) >> 8; int b = hashCode & 0x0000FF; return Color.rgb(r, g, b); } + public static void setCicleBackground(ImageView view, int color) { + ShapeDrawable circle = new ShapeDrawable(new OvalShape()); + circle.getPaint().setColor(color); + circle.setBounds(0, 0, view.getWidth(), view.getHeight()); + view.setBackground(circle); + } + + public static int randInt() { + return randInt(0, Integer.MAX_VALUE); + } + + /** + * Returns a pseudo-random number between min and max, inclusive. + * The difference between min and max can be at most + * Integer.MAX_VALUE - 1. + * + * @param min Minimum value + * @param max Maximum value. Must be greater than min. + * @return Integer between min and max, inclusive. + * @see java.util.Random#nextInt(int) + */ + public static int randInt(int min, int max) { + // nextInt is normally exclusive of the top value, + // so add 1 to make it inclusive + return rand.nextInt((max - min) + 1) + min; + } + public static String getCapitals(CharSequence sequence, int amount) { StringBuilder builder = new StringBuilder(); for (int i = 0, l = sequence.length(); i < l && builder.length() < amount; i++) { diff --git a/android/TriblerApp/app/src/main/java/org/tribler/android/TriblerViewAdapter.java b/android/TriblerApp/app/src/main/java/org/tribler/android/TriblerViewAdapter.java index 1b2ad52f7fc..3f2fb68ddf0 100644 --- a/android/TriblerApp/app/src/main/java/org/tribler/android/TriblerViewAdapter.java +++ b/android/TriblerApp/app/src/main/java/org/tribler/android/TriblerViewAdapter.java @@ -1,7 +1,5 @@ package org.tribler.android; -import android.graphics.drawable.ShapeDrawable; -import android.graphics.drawable.shapes.OvalShape; import android.net.Uri; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; @@ -183,14 +181,7 @@ public void onBindViewHolder(final RecyclerView.ViewHolder viewHolder, int adapt holder.icon.setImageURI(Uri.fromFile(icon)); } else { holder.nameCapital.setText(MyUtils.getCapitals(holder.channel.getName(), 2)); - try { - int color = MyUtils.getColor(holder.channel.hashCode()); - ShapeDrawable circle = new ShapeDrawable(new OvalShape()); - circle.getPaint().setColor(color); - circle.setBounds(0, 0, holder.icon.getWidth(), holder.icon.getHeight()); - holder.icon.setBackground(circle); - } catch (Exception ex) { - } + MyUtils.setCicleBackground(holder.icon, MyUtils.getColor(holder.channel.hashCode())); } holder.view.setOnClickListener(view -> { if (_clickListener != null) { diff --git a/android/TriblerApp/app/src/main/java/org/tribler/android/restapi/IRestApi.java b/android/TriblerApp/app/src/main/java/org/tribler/android/restapi/IRestApi.java index 176935d4159..8df00e03796 100644 --- a/android/TriblerApp/app/src/main/java/org/tribler/android/restapi/IRestApi.java +++ b/android/TriblerApp/app/src/main/java/org/tribler/android/restapi/IRestApi.java @@ -15,6 +15,7 @@ import java.io.Serializable; import retrofit2.http.DELETE; +import retrofit2.http.Field; import retrofit2.http.GET; import retrofit2.http.POST; import retrofit2.http.PUT; @@ -41,10 +42,18 @@ Observable search( Observable getMyChannel(); @POST("/mychannel") - Observable editMyChannel(); + Observable editMyChannel( + @Field("name") String name, + @Field("description") String description + ); @PUT("/channels/discovered") - Observable createChannel(); + //@FormUrlEncoded + Observable createChannel( + @Field("name") String name, + @Field("description") String description + //@Field("mode") String mode + ); @GET("/channels/popular") Observable getPopularChannels( diff --git a/android/TriblerApp/app/src/main/java/org/tribler/android/restapi/json/MyChannelResponse.java b/android/TriblerApp/app/src/main/java/org/tribler/android/restapi/json/MyChannelResponse.java index 27bbb727f04..1242eb3c43f 100644 --- a/android/TriblerApp/app/src/main/java/org/tribler/android/restapi/json/MyChannelResponse.java +++ b/android/TriblerApp/app/src/main/java/org/tribler/android/restapi/json/MyChannelResponse.java @@ -2,33 +2,13 @@ public class MyChannelResponse { - private OverviewPart overview; + private ChannelOverviewPart overview; MyChannelResponse() { } - public OverviewPart getOverview() { + public ChannelOverviewPart getOverview() { return overview; } - class OverviewPart { - - private String name, description, identifier; - - OverviewPart() { - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - public String getIdentifier() { - return identifier; - } - } - } \ No newline at end of file diff --git a/android/TriblerApp/app/src/main/res/layout/activity_edit_channel.xml b/android/TriblerApp/app/src/main/res/layout/activity_edit_channel.xml new file mode 100644 index 00000000000..870a5b2b83f --- /dev/null +++ b/android/TriblerApp/app/src/main/res/layout/activity_edit_channel.xml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +