diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6d927ae32..08c034216 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -404,6 +404,17 @@ android:value=".FDroid" /> + + + { + + private final int VIEWTYPE_HEADER = 0; + private final int VIEWTYPE_SCREENSHOTS = 1; + private final int VIEWTYPE_WHATS_NEW = 2; + + private final Context mContext; + private ArrayList mItems; + + public AppDetailsRecyclerViewAdapter(Context context) { + mContext = context; + updateItems(); + } + + private void updateItems() { + if (mItems == null) + mItems = new ArrayList<>(); + else + mItems.clear(); + mItems.add(Integer.valueOf(VIEWTYPE_HEADER)); + mItems.add(Integer.valueOf(VIEWTYPE_SCREENSHOTS)); + mItems.add(Integer.valueOf(VIEWTYPE_WHATS_NEW)); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == VIEWTYPE_HEADER) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.app_details2_header, parent, false); + return new HeaderViewHolder(view); + } else if (viewType == VIEWTYPE_SCREENSHOTS) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.app_details2_screenshots, parent, false); + return new ScreenShotsViewHolder(view); + } else if (viewType == VIEWTYPE_WHATS_NEW) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.app_details2_whatsnew, parent, false); + return new WhatsNewViewHolder(view); + } + return null; + } + + @Override + public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) { + int viewType = mItems.get(position); + if (viewType == VIEWTYPE_HEADER) { + final HeaderViewHolder vh = (HeaderViewHolder) holder; + ImageLoader.getInstance().displayImage(mApp.iconUrlLarge, vh.iconView, vh.displayImageOptions); + vh.titleView.setText(mApp.name); + if (!TextUtils.isEmpty(mApp.author)) { + vh.authorView.setText(getString(R.string.by_author) + " " + mApp.author); + vh.authorView.setVisibility(View.VISIBLE); + } else { + vh.authorView.setVisibility(View.GONE); + } + vh.summaryView.setText(mApp.summary); + final Spanned desc = Html.fromHtml(mApp.description, null, new Utils.HtmlTagHandler()); + vh.descriptionView.setMovementMethod(AppDetails2.SafeLinkMovementMethod.getInstance(mContext)); + vh.descriptionView.setText(trimNewlines(desc)); + vh.descriptionView.post(new Runnable() { + @Override + public void run() { + if (vh.descriptionView.getLineCount() < HeaderViewHolder.MAX_LINES) { + vh.descriptionMoreView.setVisibility(View.GONE); + } else { + vh.descriptionMoreView.setVisibility(View.VISIBLE); + } + } + }); + vh.buttonSecondaryView.setText(R.string.menu_uninstall); + vh.buttonSecondaryView.setVisibility(mApp.isInstalled() ? View.VISIBLE : View.INVISIBLE); + vh.buttonPrimaryView.setText(R.string.menu_install); + vh.buttonPrimaryView.setVisibility(View.VISIBLE); + +/* if (appDetails.activeDownloadUrlString != null) { + btMain.setText(R.string.downloading); + btMain.setEnabled(false); + } else if (!app.isInstalled() && app.suggestedVersionCode > 0 && + appDetails.adapter.getCount() > 0) { + // Check count > 0 due to incompatible apps resulting in an empty list. + // If App isn't installed + installed = false; + statusView.setText(R.string.details_notinstalled); + NfcHelper.disableAndroidBeam(appDetails); + // Set Install button and hide second button + btMain.setText(R.string.menu_install); + btMain.setOnClickListener(mOnClickListener); + btMain.setEnabled(true); + } else if (app.isInstalled()) { + // If App is installed + installed = true; + statusView.setText(getString(R.string.details_installed, app.installedVersionName)); + NfcHelper.setAndroidBeam(appDetails, app.packageName); + if (app.canAndWantToUpdate(appDetails)) { + updateWanted = true; + btMain.setText(R.string.menu_upgrade); + } else { + updateWanted = false; + if (appDetails.packageManager.getLaunchIntentForPackage(app.packageName) != null) { + btMain.setText(R.string.menu_launch); + } else { + btMain.setText(R.string.menu_uninstall); + } + } + btMain.setOnClickListener(mOnClickListener); + btMain.setEnabled(true); + } + + TextView currentVersion = (TextView) view.findViewById(R.id.current_version); + if (!appDetails.getApks().isEmpty()) { + currentVersion.setText(appDetails.getApks().getItem(0).versionName + " (" + app.license + ")"); + } else { + currentVersion.setVisibility(View.GONE); + btMain.setVisibility(View.GONE); + }*/ + + } else if (viewType == VIEWTYPE_SCREENSHOTS) { + ScreenShotsViewHolder vh = (ScreenShotsViewHolder) holder; + LinearLayoutManager lm = new LinearLayoutManager(mContext, LinearLayoutManager.HORIZONTAL, false); + vh.recyclerView.setLayoutManager(lm); + ScreenShotsRecyclerViewAdapter adapter = new ScreenShotsRecyclerViewAdapter(mApp); + vh.recyclerView.setAdapter(adapter); + vh.recyclerView.setHasFixedSize(true); + vh.recyclerView.setNestedScrollingEnabled(false); + LinearLayoutManagerSnapHelper helper = new LinearLayoutManagerSnapHelper(lm); + helper.setLinearSnapHelperListener(adapter); + helper.attachToRecyclerView(vh.recyclerView); + } else if (viewType == VIEWTYPE_WHATS_NEW) { + WhatsNewViewHolder vh = (WhatsNewViewHolder) holder; + vh.textView.setText("WHATS NEW GOES HERE"); + } + } + + @Override + public int getItemCount() { + return mItems.size(); + } + + @Override + public int getItemViewType(int position) { + return mItems.get(position); + } + + public class HeaderViewHolder extends RecyclerView.ViewHolder { + private static final int MAX_LINES = 5; + + final ImageView iconView; + final TextView titleView; + final TextView authorView; + final TextView summaryView; + final TextView descriptionView; + final TextView descriptionMoreView; + final Button buttonPrimaryView; + final Button buttonSecondaryView; + final DisplayImageOptions displayImageOptions; + + 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); + descriptionView = (TextView) view.findViewById(R.id.description); + descriptionMoreView = (TextView) view.findViewById(R.id.description_more); + buttonPrimaryView = (Button) view.findViewById(R.id.primaryButtonView); + buttonSecondaryView = (Button) view.findViewById(R.id.secondaryButtonView); + displayImageOptions = new DisplayImageOptions.Builder() + .cacheInMemory(true) + .cacheOnDisk(true) + .imageScaleType(ImageScaleType.NONE) + .showImageOnLoading(R.drawable.ic_repo_app_default) + .showImageForEmptyUri(R.drawable.ic_repo_app_default) + .bitmapConfig(Bitmap.Config.RGB_565) + .build(); + descriptionView.setMaxLines(MAX_LINES); + descriptionView.setEllipsize(TextUtils.TruncateAt.MARQUEE); + descriptionMoreView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // Remember current scroll position so that we can restore it + LinearLayoutManager lm = (LinearLayoutManager)mRecyclerView.getLayoutManager(); + int pos = lm.findFirstVisibleItemPosition(); + int posOffset = 0; + if (pos != NO_POSITION) { + View view = lm.findViewByPosition(pos); + if (view != null) + posOffset = lm.getDecoratedTop(view); + } + if (TextViewCompat.getMaxLines(descriptionView) != MAX_LINES) { + descriptionView.setMaxLines(MAX_LINES); + descriptionMoreView.setText(R.string.more); + } else { + descriptionView.setMaxLines(Integer.MAX_VALUE); + descriptionMoreView.setText(R.string.less); + } + if (pos != NO_POSITION) { + // Restore scroll position + lm.scrollToPositionWithOffset(pos, posOffset); + } + } + }); + // Set ALL caps (in a way compatible with SDK 10) + AllCapsTransformationMethod allCapsTransformation = new AllCapsTransformationMethod(view.getContext()); + buttonPrimaryView.setTransformationMethod(allCapsTransformation); + buttonSecondaryView.setTransformationMethod(allCapsTransformation); + descriptionMoreView.setTransformationMethod(allCapsTransformation); + } + + @Override + public String toString() { + return super.toString() + " '" + titleView.getText() + "'"; + } + } + + public class ScreenShotsViewHolder extends RecyclerView.ViewHolder { + final RecyclerView recyclerView; + + ScreenShotsViewHolder(View view) { + super(view); + recyclerView = (RecyclerView) view.findViewById(R.id.screenshots); + } + + @Override + public String toString() { + return super.toString() + " screenshots"; + } + } + + public class WhatsNewViewHolder extends RecyclerView.ViewHolder { + final TextView textView; + + WhatsNewViewHolder(View view) { + super(view); + textView = (TextView) view.findViewById(R.id.text); + } + + @Override + public String toString() { + return super.toString() + " " + textView.getText(); + } + } + + class ScreenShotsRecyclerViewAdapter extends RecyclerView.Adapter implements LinearLayoutManagerSnapHelper.LinearSnapHelperListener { + private final App app; + private final DisplayImageOptions displayImageOptions; + private View selectedView; + private int selectedPosition; + private int selectedItemElevation; + private int unselectedItemMargin; + + public ScreenShotsRecyclerViewAdapter(App app) { + super(); + this.app = app; + selectedPosition = 0; + selectedItemElevation = getResources().getDimensionPixelSize(R.dimen.details_screenshot_selected_elevation); + unselectedItemMargin = getResources().getDimensionPixelSize(R.dimen.details_screenshot_margin); + displayImageOptions = new DisplayImageOptions.Builder() + .cacheInMemory(true) + .cacheOnDisk(true) + .imageScaleType(ImageScaleType.NONE) + .showImageOnLoading(R.drawable.ic_repo_app_default) + .showImageForEmptyUri(R.drawable.ic_repo_app_default) + .bitmapConfig(Bitmap.Config.RGB_565) + .build(); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + ScreenShotViewHolder vh = (ScreenShotViewHolder) holder; + setViewSelected(vh.itemView, position == selectedPosition); + if (position == selectedPosition) + this.selectedView = vh.itemView; + ImageLoader.getInstance().displayImage(mApp.iconUrlLarge, vh.image, displayImageOptions); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.app_details2_screenshot_item, parent, false); + return new ScreenShotViewHolder(view); + } + + @Override + public int getItemCount() { + return 7; + } + + @Override + public void onSnappedToView(View view, int snappedPosition) { + setViewSelected(selectedView, false); + selectedView = view; + selectedPosition = snappedPosition; + setViewSelected(selectedView, true); + } + + private void setViewSelected(View view, boolean selected) { + if (view != null) { + RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)view.getLayoutParams(); + if (selected) + lp.setMargins(0,selectedItemElevation,0,selectedItemElevation); + else + lp.setMargins(0,unselectedItemMargin,0,unselectedItemMargin); + ViewCompat.setElevation(view, selected ? selectedItemElevation : 0); + view.setLayoutParams(lp); + } + } + + public class ScreenShotViewHolder extends RecyclerView.ViewHolder { + final ImageView image; + + ScreenShotViewHolder(View view) { + super(view); + image = (ImageView) view.findViewById(R.id.image); + } + + @Override + public String toString() { + return super.toString() + " screenshot"; + } + } + } + } + + // The HTML formatter adds "\n\n" at the end of every paragraph. This + // is desired between paragraphs, but not at the end of the whole + // string as it adds unwanted spacing at the end of the TextView. + // Remove all trailing newlines. + // Use this function instead of a trim() as that would require + // converting to String and thus losing formatting (e.g. bold). + private static CharSequence trimNewlines(CharSequence s) { + if (s == null || s.length() < 1) { + return s; + } + int i; + for (i = s.length() - 1; i >= 0; i--) { + if (s.charAt(i) != '\n') { + break; + } + } + if (i == s.length() - 1) { + return s; + } + return s.subSequence(0, i + 1); + } + + private static final class SafeLinkMovementMethod extends LinkMovementMethod { + + private static AppDetails2.SafeLinkMovementMethod instance; + + private final Context ctx; + + private SafeLinkMovementMethod(Context ctx) { + this.ctx = ctx; + } + + public static AppDetails2.SafeLinkMovementMethod getInstance(Context ctx) { + if (instance == null) { + instance = new AppDetails2.SafeLinkMovementMethod(ctx); + } + return instance; + } + + private static CharSequence getLink(TextView widget, Spannable buffer, + MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + x -= widget.getTotalPaddingLeft(); + y -= widget.getTotalPaddingTop(); + x += widget.getScrollX(); + y += widget.getScrollY(); + + Layout layout = widget.getLayout(); + final int line = layout.getLineForVertical(y); + final int off = layout.getOffsetForHorizontal(line, x); + final ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class); + + if (links.length > 0) { + final ClickableSpan link = links[0]; + final Spanned s = (Spanned) widget.getText(); + return s.subSequence(s.getSpanStart(link), s.getSpanEnd(link)); + } + return "null"; + } + + @Override + public boolean onTouchEvent(@NonNull TextView widget, @NonNull Spannable buffer, + @NonNull MotionEvent event) { + try { + return super.onTouchEvent(widget, buffer, event); + } catch (ActivityNotFoundException ex) { + Selection.removeSelection(buffer); + final CharSequence link = getLink(widget, buffer, event); + Toast.makeText(ctx, + ctx.getString(R.string.no_handler_app, link), + Toast.LENGTH_LONG).show(); + return true; + } + } + + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index eca09c6bd..fec4b0e6d 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -20,6 +20,7 @@ package org.fdroid.fdroid; import android.content.Context; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; @@ -31,6 +32,7 @@ import android.text.Html; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; +import android.util.TypedValue; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.assist.ImageScaleType; @@ -596,4 +598,9 @@ public final class Utils { return data; } + public static int dpToPx(int dp, Context ctx) + { + Resources r = ctx.getResources(); + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()); + } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/LinearLayoutManagerSnapHelper.java b/app/src/main/java/org/fdroid/fdroid/views/LinearLayoutManagerSnapHelper.java new file mode 100644 index 000000000..ebbecf2eb --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/LinearLayoutManagerSnapHelper.java @@ -0,0 +1,74 @@ +package org.fdroid.fdroid.views; + +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.LinearSnapHelper; +import android.support.v7.widget.OrientationHelper; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import static android.support.v7.widget.RecyclerView.NO_POSITION; + +public class LinearLayoutManagerSnapHelper extends LinearSnapHelper { + + public interface LinearSnapHelperListener { + /** + * Tells the listener that we have selected a view to snap to. + * @param view The selected view (may be null) + * @param position Adapter position of the snapped to view (or NO_POSITION if none) + */ + void onSnappedToView(View view, int position); + }; + + private LinearLayoutManager mLlm; + private OrientationHelper mOrientationHelper; + private LinearSnapHelperListener mListener; + + public LinearLayoutManagerSnapHelper(LinearLayoutManager llm) { + this.mLlm = llm; + this.mOrientationHelper = OrientationHelper.createHorizontalHelper(mLlm); + }; + + public void setLinearSnapHelperListener(LinearSnapHelperListener listener) { + mListener = listener; + } + + @Override + public View findSnapView(RecyclerView.LayoutManager layoutManager) { + View snappedView = super.findSnapView(layoutManager); + if (layoutManager.canScrollHorizontally()) { + if (layoutManager instanceof LinearLayoutManager) { + int firstChild = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); + int lastChild = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); + if (firstChild == 0) { + View child = layoutManager.findViewByPosition(firstChild); + if (mOrientationHelper.getDecoratedEnd(child) >= mOrientationHelper.getDecoratedMeasurement(child) / 2 + && mOrientationHelper.getDecoratedEnd(child) > 0) { + int dist1 = super.calculateDistanceToFinalSnap(layoutManager, snappedView)[0]; + int dist2 = mOrientationHelper.getDecoratedStart(child); + if (Math.abs(dist1) > Math.abs(dist2)) { + snappedView = child; + } + } + } else if (lastChild == (mLlm.getItemCount() - 1)) { + View child = layoutManager.findViewByPosition(lastChild); + if (mOrientationHelper.getDecoratedStart(child) < mOrientationHelper.getTotalSpace() - mOrientationHelper.getDecoratedMeasurement(child) / 2 + && mOrientationHelper.getDecoratedStart(child) < mOrientationHelper.getTotalSpace()) { + int dist1 = super.calculateDistanceToFinalSnap(layoutManager, snappedView)[0]; + int dist2 = mOrientationHelper.getTotalSpace() - mOrientationHelper.getDecoratedEnd(child); + if (Math.abs(dist1) > Math.abs(dist2)) { + snappedView = child; + } + } + } + } + } + if (mListener != null) { + int snappedPosition = 0; + if (snappedView != null) + snappedPosition = mLlm.getPosition(snappedView); + mListener.onSnappedToView(snappedView, snappedPosition); + } + return snappedView; + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/fragments/AppListFragment.java b/app/src/main/java/org/fdroid/fdroid/views/fragments/AppListFragment.java index 188861f01..702b2470d 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/fragments/AppListFragment.java +++ b/app/src/main/java/org/fdroid/fdroid/views/fragments/AppListFragment.java @@ -20,6 +20,7 @@ import android.widget.AdapterView; import android.widget.TextView; import org.fdroid.fdroid.AppDetails; +import org.fdroid.fdroid.AppDetails2; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; @@ -30,6 +31,7 @@ import org.fdroid.fdroid.views.AppListAdapter; public abstract class AppListFragment extends ListFragment implements AdapterView.OnItemClickListener, + AdapterView.OnItemLongClickListener, Preferences.ChangeListener, LoaderManager.LoaderCallbacks { @@ -109,6 +111,7 @@ public abstract class AppListFragment extends ListFragment implements // returns the list view is "called between onCreate and // onActivityCreated" according to the docs. getListView().setOnItemClickListener(this); + getListView().setOnItemLongClickListener(this); } @Override @@ -155,11 +158,21 @@ public abstract class AppListFragment extends ListFragment implements @Override public void onItemClick(AdapterView parent, View view, int position, long id) { + showItemDetails(view, position, false); + } + + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + showItemDetails(view, position, true); + return true; + } + + private void showItemDetails(View view, int position, boolean useNewDetailsActivity) { // Cursor is null in the swap list when touching the first item. Cursor cursor = (Cursor) getListView().getItemAtPosition(position); if (cursor != null) { final App app = new App(cursor); - Intent intent = getAppDetailsIntent(); + Intent intent = getAppDetailsIntent(useNewDetailsActivity); intent.putExtra(AppDetails.EXTRA_APPID, app.packageName); intent.putExtra(AppDetails.EXTRA_FROM, getFromTitle()); if (Build.VERSION.SDK_INT >= 21) { @@ -176,8 +189,8 @@ public abstract class AppListFragment extends ListFragment implements } } - private Intent getAppDetailsIntent() { - return new Intent(getActivity(), AppDetails.class); + private Intent getAppDetailsIntent(boolean useNewDetailsActivity) { + return new Intent(getActivity(), useNewDetailsActivity ? AppDetails2.class : AppDetails.class); } @Override diff --git a/app/src/main/res/drawable-mdpi/feature_placeholder.png b/app/src/main/res/drawable-mdpi/feature_placeholder.png new file mode 100644 index 000000000..861559285 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/feature_placeholder.png differ diff --git a/app/src/main/res/drawable/button_primary_background_selector.xml b/app/src/main/res/drawable/button_primary_background_selector.xml new file mode 100644 index 000000000..ee21a1543 --- /dev/null +++ b/app/src/main/res/drawable/button_primary_background_selector.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_secondary_background_selector.xml b/app/src/main/res/drawable/button_secondary_background_selector.xml new file mode 100644 index 000000000..2cb0d8ebb --- /dev/null +++ b/app/src/main/res/drawable/button_secondary_background_selector.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_app_details2.xml b/app/src/main/res/layout/activity_app_details2.xml new file mode 100644 index 000000000..d71539052 --- /dev/null +++ b/app/src/main/res/layout/activity_app_details2.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/app_details2_header.xml b/app/src/main/res/layout/app_details2_header.xml new file mode 100755 index 000000000..8a2217e4a --- /dev/null +++ b/app/src/main/res/layout/app_details2_header.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + +