diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java index 24c9701e1..a705211a4 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java @@ -13,6 +13,7 @@ import android.graphics.Bitmap; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; +import android.support.design.widget.AppBarLayout; import android.support.design.widget.CoordinatorLayout; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.AlertDialog; @@ -46,6 +47,7 @@ import org.fdroid.fdroid.installer.InstallerService; import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.DownloaderService; import org.fdroid.fdroid.views.AppDetailsRecyclerViewAdapter; +import org.fdroid.fdroid.views.OverscrollLinearLayoutManager; import org.fdroid.fdroid.views.ShareChooserDialog; import org.fdroid.fdroid.views.apps.FeatureImage; @@ -59,6 +61,8 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog private FDroidApp fdroidApp; private App app; + private CoordinatorLayout coordinatorLayout; + private AppBarLayout appBarLayout; private RecyclerView recyclerView; private AppDetailsRecyclerViewAdapter adapter; private LocalBroadcastManager localBroadcastManager; @@ -91,10 +95,42 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog localBroadcastManager = LocalBroadcastManager.getInstance(this); + coordinatorLayout = (CoordinatorLayout) findViewById(R.id.rootCoordinator); + appBarLayout = (AppBarLayout) coordinatorLayout.findViewById(R.id.app_bar); recyclerView = (RecyclerView) findViewById(R.id.rvDetails); adapter = new AppDetailsRecyclerViewAdapter(this, app, this); - LinearLayoutManager lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); + OverscrollLinearLayoutManager lm = new OverscrollLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); lm.setStackFromEnd(false); + + /** The recyclerView/AppBarLayout combo has a bug that prevents a "fling" from the bottom + * to continue all the way to the top by expanding the AppBarLayout. It will instead stop + * with the app bar in a collapsed state. See here: https://code.google.com/p/android/issues/detail?id=177729 + * Not sure this is the exact issue, but it is true that while in a fling the RecyclerView will + * consume the scroll events quietly, without calling the nested scrolling mechanism. + * We fix this behavior by using an OverscrollLinearLayoutManager that will give us information + * of overscroll, i.e. when we have not consumed all of a scroll event, and use this information + * to send the scroll to the app bar layout so that it will expand itself. + */ + lm.setOnOverscrollListener(new OverscrollLinearLayoutManager.OnOverscrollListener() { + @Override + public int onOverscrollX(int overscroll) { + return 0; + } + + @Override + public int onOverscrollY(int overscroll) { + int consumed = 0; + if (overscroll < 0) { + CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams(); + CoordinatorLayout.Behavior behavior = lp.getBehavior(); + if (behavior != null && behavior instanceof AppBarLayout.Behavior) { + ((AppBarLayout.Behavior) behavior).onNestedScroll(coordinatorLayout, appBarLayout, recyclerView, 0, 0, 0, overscroll); + consumed = overscroll; // Consume all of it! + } + } + return consumed; + } + }); recyclerView.setLayoutManager(lm); recyclerView.setAdapter(adapter); diff --git a/app/src/main/java/org/fdroid/fdroid/data/Apk.java b/app/src/main/java/org/fdroid/fdroid/data/Apk.java index d1f75ff89..e08078756 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Apk.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Apk.java @@ -9,6 +9,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; +import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.RepoXMLHandler; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Schema.ApkTable.Cols; @@ -74,6 +75,11 @@ public class Apk extends ValueObject implements Comparable, Parcelable { public String repoAddress; public String[] incompatibleReasons; + /** + * A descriptive text for what has changed in the latest version. + */ + public String whatsNew; + public String[] antiFeatures; /** @@ -211,6 +217,13 @@ public class Apk extends ValueObject implements Comparable, Parcelable { break; } } + + // For now, just populate "what's new" with placeholder (or leave blank) + if (BuildConfig.DEBUG) { + if (Math.random() > 0.5) { + whatsNew = "This section will contain the 'what's new' information for the apk.\n\n\t• Bug fixes."; + } + } } private void checkRepoAddress() { diff --git a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java index 102c22888..e64e3a6a9 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java @@ -50,7 +50,10 @@ import org.fdroid.fdroid.privileged.views.AppSecurityPermissions; import java.text.NumberFormat; import java.util.ArrayList; +import java.util.Calendar; import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; public class AppDetailsRecyclerViewAdapter extends RecyclerView.Adapter { @@ -81,12 +84,11 @@ public class AppDetailsRecyclerViewAdapter private static final int VIEWTYPE_HEADER = 0; private static final int VIEWTYPE_SCREENSHOTS = 1; - private static final int VIEWTYPE_WHATS_NEW = 2; - private static final int VIEWTYPE_DONATE = 3; - private static final int VIEWTYPE_LINKS = 4; - private static final int VIEWTYPE_PERMISSIONS = 5; - private static final int VIEWTYPE_VERSIONS = 6; - private static final int VIEWTYPE_VERSION = 7; + private static final int VIEWTYPE_DONATE = 2; + private static final int VIEWTYPE_LINKS = 3; + private static final int VIEWTYPE_PERMISSIONS = 4; + private static final int VIEWTYPE_VERSIONS = 5; + private static final int VIEWTYPE_VERSION = 6; private final Context context; @NonNull @@ -125,7 +127,6 @@ public class AppDetailsRecyclerViewAdapter } addItem(VIEWTYPE_HEADER); addItem(VIEWTYPE_SCREENSHOTS); - addItem(VIEWTYPE_WHATS_NEW); addItem(VIEWTYPE_DONATE); addItem(VIEWTYPE_LINKS); addItem(VIEWTYPE_PERMISSIONS); @@ -167,6 +168,12 @@ public class AppDetailsRecyclerViewAdapter private boolean shouldShowPermissions() { // Figure out if we should show permissions section + Apk curApk = getSuggestedApk(); + final boolean curApkCompatible = curApk != null && curApk.compatible; + return versions.size() > 0 && (curApkCompatible || Preferences.get().showIncompatibleVersions()); + } + + private Apk getSuggestedApk() { Apk curApk = null; for (int i = 0; i < versions.size(); i++) { final Apk apk = versions.get(i); @@ -175,8 +182,7 @@ public class AppDetailsRecyclerViewAdapter break; } } - final boolean curApkCompatible = curApk != null && curApk.compatible; - return versions.size() > 0 && (curApkCompatible || Preferences.get().showIncompatibleVersions()); + return curApk; } private boolean shouldShowDonate() { @@ -206,9 +212,6 @@ public class AppDetailsRecyclerViewAdapter case VIEWTYPE_SCREENSHOTS: View screenshots = inflater.inflate(R.layout.app_details2_screenshots, parent, false); return new ScreenShotsViewHolder(screenshots); - case VIEWTYPE_WHATS_NEW: - View whatsNew = inflater.inflate(R.layout.app_details2_whatsnew, parent, false); - return new WhatsNewViewHolder(whatsNew); case VIEWTYPE_DONATE: View donate = inflater.inflate(R.layout.app_details2_donate, parent, false); return new DonateViewHolder(donate); @@ -240,9 +243,6 @@ public class AppDetailsRecyclerViewAdapter case VIEWTYPE_SCREENSHOTS: ((ScreenShotsViewHolder) holder).bindModel(); break; - case VIEWTYPE_WHATS_NEW: - ((WhatsNewViewHolder) holder).bindModel(); - break; case VIEWTYPE_DONATE: ((DonateViewHolder) holder).bindModel(); break; @@ -289,9 +289,13 @@ public class AppDetailsRecyclerViewAdapter final ImageView iconView; final TextView titleView; final TextView authorView; - final TextView summaryView; + final TextView lastUpdateView; + final TextView whatsNewView; final TextView descriptionView; final TextView descriptionMoreView; + final TextView antiFeaturesLabelView; + final TextView antiFeaturesView; + final View antiFeaturesWarningView; final View buttonLayout; final Button buttonPrimaryView; final Button buttonSecondaryView; @@ -301,15 +305,20 @@ public class AppDetailsRecyclerViewAdapter final TextView progressPercent; final View progressCancel; final DisplayImageOptions displayImageOptions; + boolean descriptionIsExpanded; HeaderViewHolder(View view) { super(view); iconView = (ImageView) view.findViewById(R.id.icon); titleView = (TextView) view.findViewById(R.id.title); authorView = (TextView) view.findViewById(R.id.author); - summaryView = (TextView) view.findViewById(R.id.summary); + lastUpdateView = (TextView) view.findViewById(R.id.text_last_update); + whatsNewView = (TextView) view.findViewById(R.id.whats_new); descriptionView = (TextView) view.findViewById(R.id.description); descriptionMoreView = (TextView) view.findViewById(R.id.description_more); + antiFeaturesLabelView = (TextView) view.findViewById(R.id.label_anti_features); + antiFeaturesView = (TextView) view.findViewById(R.id.text_anti_features); + antiFeaturesWarningView = view.findViewById(R.id.anti_features_warning); buttonLayout = view.findViewById(R.id.button_layout); buttonPrimaryView = (Button) view.findViewById(R.id.primaryButtonView); buttonSecondaryView = (Button) view.findViewById(R.id.secondaryButtonView); @@ -339,10 +348,13 @@ public class AppDetailsRecyclerViewAdapter if (TextViewCompat.getMaxLines(descriptionView) != MAX_LINES) { descriptionView.setMaxLines(MAX_LINES); descriptionMoreView.setText(R.string.more); + descriptionIsExpanded = false; } else { descriptionView.setMaxLines(Integer.MAX_VALUE); descriptionMoreView.setText(R.string.less); + descriptionIsExpanded = true; } + updateAntiFeaturesWarning(); } }); // Set ALL caps (in a way compatible with SDK 10) @@ -392,7 +404,28 @@ public class AppDetailsRecyclerViewAdapter } else { authorView.setVisibility(View.GONE); } - summaryView.setText(app.summary); + if (app.lastUpdated != null) { + long msDiff = Calendar.getInstance().getTimeInMillis() - app.lastUpdated.getTime(); + int daysDiff = (int) TimeUnit.MILLISECONDS.toDays(msDiff); + lastUpdateView.setText(lastUpdateView.getContext().getResources().getQuantityString(R.plurals.details_last_update_days, daysDiff, daysDiff)); + lastUpdateView.setVisibility(View.VISIBLE); + } else { + lastUpdateView.setVisibility(View.GONE); + } + Apk suggestedApk = getSuggestedApk(); + if (suggestedApk == null || TextUtils.isEmpty(suggestedApk.whatsNew)) { + whatsNewView.setVisibility(View.GONE); + } else { + //noinspection deprecation Ignore deprecation because the suggested way is only available in API 24. + Locale locale = context.getResources().getConfiguration().locale; + + StringBuilder sbWhatsNew = new StringBuilder(); + sbWhatsNew.append(whatsNewView.getContext().getString(R.string.details_new_in_version, suggestedApk.versionName).toUpperCase(locale)); + sbWhatsNew.append("\n\n"); + sbWhatsNew.append(suggestedApk.whatsNew); + whatsNewView.setText(sbWhatsNew); + whatsNewView.setVisibility(View.VISIBLE); + } final Spanned desc = Html.fromHtml(app.description, null, new Utils.HtmlTagHandler()); descriptionView.setMovementMethod(LinkMovementMethod.getInstance()); descriptionView.setText(trimTrailingNewlines(desc)); @@ -412,13 +445,27 @@ public class AppDetailsRecyclerViewAdapter descriptionView.post(new Runnable() { @Override public void run() { - if (descriptionView.getLineCount() <= HeaderViewHolder.MAX_LINES) { + if (descriptionView.getLineCount() <= HeaderViewHolder.MAX_LINES && app.antiFeatures == null) { descriptionMoreView.setVisibility(View.GONE); } else { descriptionMoreView.setVisibility(View.VISIBLE); } } }); + if (app.antiFeatures != null) { + StringBuilder sb = new StringBuilder(); + for (String af : app.antiFeatures) { + String afdesc = descAntiFeature(af); + sb.append("\t• ").append(afdesc).append('\n'); + } + if (sb.length() > 0) { + sb.setLength(sb.length() - 1); + antiFeaturesView.setText(sb.toString()); + } else { + antiFeaturesView.setVisibility(View.GONE); + } + } + updateAntiFeaturesWarning(); buttonSecondaryView.setText(R.string.menu_uninstall); buttonSecondaryView.setVisibility(app.isInstalled() ? View.VISIBLE : View.INVISIBLE); buttonSecondaryView.setOnClickListener(onUnInstallClickListener); @@ -464,6 +511,39 @@ public class AppDetailsRecyclerViewAdapter }); } + + private void updateAntiFeaturesWarning() { + if (app.antiFeatures == null || TextUtils.isEmpty(antiFeaturesView.getText())) { + antiFeaturesLabelView.setVisibility(View.GONE); + antiFeaturesView.setVisibility(View.GONE); + antiFeaturesWarningView.setVisibility(View.GONE); + } else { + antiFeaturesLabelView.setVisibility(descriptionIsExpanded ? View.VISIBLE : View.GONE); + antiFeaturesView.setVisibility(descriptionIsExpanded ? View.VISIBLE : View.GONE); + antiFeaturesWarningView.setVisibility(descriptionIsExpanded ? View.GONE : View.VISIBLE); + } + } + + private String descAntiFeature(String af) { + switch (af) { + case "Ads": + return itemView.getContext().getString(R.string.antiadslist); + case "Tracking": + return itemView.getContext().getString(R.string.antitracklist); + case "NonFreeNet": + return itemView.getContext().getString(R.string.antinonfreenetlist); + case "NonFreeAdd": + return itemView.getContext().getString(R.string.antinonfreeadlist); + case "NonFreeDep": + return itemView.getContext().getString(R.string.antinonfreedeplist); + case "UpstreamNonFree": + return itemView.getContext().getString(R.string.antiupstreamnonfreelist); + case "NonFreeAssets": + return itemView.getContext().getString(R.string.antinonfreeassetslist); + default: + return af; + } + } } @Override @@ -503,19 +583,6 @@ public class AppDetailsRecyclerViewAdapter } } - private class WhatsNewViewHolder extends RecyclerView.ViewHolder { - final TextView textView; - - WhatsNewViewHolder(View view) { - super(view); - textView = (TextView) view.findViewById(R.id.text); - } - - public void bindModel() { - textView.setText("WHATS NEW GOES HERE"); - } - } - private class DonateViewHolder extends RecyclerView.ViewHolder { final TextView donateHeading; final GridLayout donationOptionsLayout; diff --git a/app/src/main/java/org/fdroid/fdroid/views/OverscrollLinearLayoutManager.java b/app/src/main/java/org/fdroid/fdroid/views/OverscrollLinearLayoutManager.java new file mode 100644 index 000000000..2af5ac1e6 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/OverscrollLinearLayoutManager.java @@ -0,0 +1,91 @@ +package org.fdroid.fdroid.views; + +import android.content.Context; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; + +/** + * This class is like a standard LinearLayoutManager but with an option to add an + * overscroll listener. This can be used to consume overscrolls, e.g. to draw custom + * "glows". + */ +public class OverscrollLinearLayoutManager extends LinearLayoutManager { + + /** + * A listener interface to get overscroll infromation. + */ + public interface OnOverscrollListener { + /** + * Notifies the listener that an overscroll has happened in the x direction. + * @param overscroll If negative, the recycler view has been scrolled to the "start" + * position. If positive to the "end" position. + * @return Return the amount of overscroll consumed. Returning 0 will let the + * recycler view handle this in the default way. Return "overscroll" to consume the + * whole event. + */ + int onOverscrollX(int overscroll); + + /** + * Notifies the listener that an overscroll has happened in the y direction. + * @param overscroll If negative, the recycler view has been scrolled to the "top" + * position. If positive to the "bottom" position. + * @return Return the amount of overscroll consumed. Returning 0 will let the + * recycler view handle this in the default way. Return "overscroll" to consume the + * whole event. + */ + + int onOverscrollY(int overscroll); + } + + private OnOverscrollListener overscrollListener = null; + + public OverscrollLinearLayoutManager(Context context) { + super(context); + } + + public OverscrollLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { + super(context, orientation, reverseLayout); + } + + public OverscrollLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + /** + * Set the {@link OverscrollLinearLayoutManager.OnOverscrollListener} to get information about + * when the parent recyclerview is overscrolled. + * + * @param listener Listener to add + * @see OverscrollLinearLayoutManager.OnOverscrollListener + */ + public void setOnOverscrollListener(OnOverscrollListener listener) { + overscrollListener = listener; + } + + @Override + public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { + int consumed = super.scrollHorizontallyBy(dx, recycler, state); + int overscrollX = dx - consumed; + if (overscrollX != 0) { + if (overscrollListener != null) { + int consumedByListener = overscrollListener.onOverscrollX(overscrollX); + consumed += consumedByListener; + } + } + return consumed; + } + + @Override + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { + int consumed = super.scrollVerticallyBy(dy, recycler, state); + int overscrollY = dy - consumed; + if (overscrollY != 0) { + if (overscrollListener != null) { + int consumedByListener = overscrollListener.onOverscrollY(overscrollY); + consumed += consumedByListener; + } + } + return consumed; + } +} diff --git a/app/src/main/res/drawable/ic_warning_black_24dp.xml b/app/src/main/res/drawable/ic_warning_black_24dp.xml new file mode 100644 index 000000000..b3a9e036b --- /dev/null +++ b/app/src/main/res/drawable/ic_warning_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/app_details2_header.xml b/app/src/main/res/layout/app_details2_header.xml index 823d69ec7..2c14167d6 100755 --- a/app/src/main/res/layout/app_details2_header.xml +++ b/app/src/main/res/layout/app_details2_header.xml @@ -12,97 +12,111 @@ + android:orientation="vertical" + android:paddingBottom="8dp" + > - + android:orientation="vertical"> - + + + + + + + + android:layout_alignParentLeft="true" + android:layout_alignParentRight="true" + android:layout_alignParentStart="true" + android:layout_below="@id/icon"> + + android:layout_centerVertical="true" + android:src="@android:drawable/ic_menu_close_clear_cancel" /> + + android:textAppearance="@style/TextAppearance.AppCompat.Small" /> + + android:textAppearance="@style/TextAppearance.AppCompat.Small" /> + + android:layout_below="@id/progress_label" + android:layout_toLeftOf="@id/progress_cancel" + android:layout_toStartOf="@id/progress_cancel" /> @@ -137,7 +150,6 @@ android:layout_marginLeft="8dp" android:layout_marginStart="8dp" android:layout_weight="1" - android:maxLines="1" android:ellipsize="marquee" android:padding="12dp" tools:text="THIS IS 2" /> @@ -146,35 +158,80 @@ + android:textAppearance="@style/TextAppearance.AppCompat.Body1" + tools:text="This is the app description of this awezome app. It can be several lines long, but will be truncated at just a few if it is. A 'read more' button will appear so that you can expand the view and view the full text, if you wish. Yes, it will be blue and beautiful." /> + + + + + + diff --git a/app/src/main/res/layout/app_details2_whatsnew.xml b/app/src/main/res/layout/app_details2_whatsnew.xml deleted file mode 100755 index 3498f984e..000000000 --- a/app/src/main/res/layout/app_details2_whatsnew.xml +++ /dev/null @@ -1,8 +0,0 @@ - - \ 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 77a11336d..8867b0f75 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -169,7 +169,10 @@ Version %s installed Not installed + New in version %s + This app has features you may not like. + Anti-features This app contains advertising This app tracks and reports your activity This app promotes non-free add-ons @@ -473,4 +476,10 @@ View the single one app in the %2$s category View all %1$d apps from the %2$s category + + + Updated today + Updated %1$s day ago + Updated %1$s days ago + diff --git a/app/src/main/res/values/styles_detail.xml b/app/src/main/res/values/styles_detail.xml index 19fa91439..2a6b07c86 100644 --- a/app/src/main/res/values/styles_detail.xml +++ b/app/src/main/res/values/styles_detail.xml @@ -34,4 +34,15 @@ @color/fdroid_blue + + + + + + \ No newline at end of file diff --git a/app/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java b/app/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java index 70ac2834f..a075e8783 100644 --- a/app/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java +++ b/app/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java @@ -47,7 +47,7 @@ public class AppDetailsAdapterTest extends FDroidProviderTest { AppDetailsRecyclerViewAdapter adapter = new AppDetailsRecyclerViewAdapter(context, app, dummyCallbacks); populateViewHolders(adapter); - assertEquals(5, adapter.getItemCount()); + assertEquals(4, adapter.getItemCount()); }