package org.fdroid.fdroid.views; import android.annotation.SuppressLint; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; import android.support.annotation.NonNull; import android.support.v4.view.ViewCompat; import android.support.v4.widget.TextViewCompat; import android.support.v7.text.AllCapsTransformationMethod; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.Html; import android.text.Spannable; import android.text.Spanned; import android.text.TextUtils; import android.text.format.DateFormat; import android.text.method.LinkMovementMethod; import android.text.style.URLSpan; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.ImageScaleType; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.InstalledAppProvider; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.privileged.views.AppDiff; import org.fdroid.fdroid.privileged.views.AppSecurityPermissions; import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; public class AppDetailsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { public interface AppDetailsRecyclerViewAdapterCallbacks { boolean isAppDownloading(); void enableAndroidBeam(); void disableAndroidBeam(); void openUrl(String url); void installApk(); void installApk(Apk apk); void upgradeApk(); void uninstallApk(); void installCancel(); void launchApk(); } 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 final Context context; @NonNull private App app; private final AppDetailsRecyclerViewAdapterCallbacks callbacks; private RecyclerView recyclerView; private ArrayList<Object> items; private ArrayList<Apk> versions; private boolean showVersions; private HeaderViewHolder headerView; public AppDetailsRecyclerViewAdapter(Context context, @NonNull App app, AppDetailsRecyclerViewAdapterCallbacks callbacks) { this.context = context; this.callbacks = callbacks; this.app = app; updateItems(app); } public void updateItems(@NonNull App app) { this.app = app; // Get versions versions = new ArrayList<>(); final List<Apk> apks = ApkProvider.Helper.findByPackageName(context, this.app.packageName); for (final Apk apk : apks) { if (apk.compatible || Preferences.get().showIncompatibleVersions()) { versions.add(apk); } } if (items == null) { items = new ArrayList<>(); } else { items.clear(); } addItem(VIEWTYPE_HEADER); addItem(VIEWTYPE_SCREENSHOTS); addItem(VIEWTYPE_WHATS_NEW); addItem(VIEWTYPE_DONATE); addItem(VIEWTYPE_LINKS); addItem(VIEWTYPE_PERMISSIONS); addItem(VIEWTYPE_VERSIONS); notifyDataSetChanged(); } private void setShowVersions(boolean showVersions) { this.showVersions = showVersions; items.removeAll(versions); if (showVersions) { items.addAll(items.indexOf(VIEWTYPE_VERSIONS) + 1, versions); } notifyDataSetChanged(); } private void addItem(int item) { // Gives us a chance to hide sections that are not used, e.g. the donate section when // we have no donation links. if (item == VIEWTYPE_DONATE && !shouldShowDonate()) { return; } else if (item == VIEWTYPE_PERMISSIONS && !shouldShowPermissions()) { return; } items.add(item); } private boolean shouldShowPermissions() { // Figure out if we should show permissions section Apk curApk = null; for (int i = 0; i < versions.size(); i++) { final Apk apk = versions.get(i); if (apk.versionCode == app.suggestedVersionCode) { curApk = apk; break; } } final boolean curApkCompatible = curApk != null && curApk.compatible; return versions.size() > 0 && (curApkCompatible || Preferences.get().showIncompatibleVersions()); } private boolean shouldShowDonate() { return uriIsSetAndCanBeOpened(app.donateURL) || uriIsSetAndCanBeOpened(app.getBitcoinUri()) || uriIsSetAndCanBeOpened(app.getLitecoinUri()) || uriIsSetAndCanBeOpened(app.getFlattrUri()); } public void clearProgress() { setProgress(0, 0, 0); } public void setProgress(int bytesDownloaded, int totalBytes, int resIdString) { if (headerView != null) { headerView.setProgress(bytesDownloaded, totalBytes, resIdString); } } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); switch (viewType) { case VIEWTYPE_HEADER: View header = inflater.inflate(R.layout.app_details2_header, parent, false); return new HeaderViewHolder(header); 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); case VIEWTYPE_LINKS: View links = inflater.inflate(R.layout.app_details2_links, parent, false); return new LinksViewHolder(links); case VIEWTYPE_PERMISSIONS: View permissions = inflater.inflate(R.layout.app_details2_links, parent, false); return new PermissionsViewHolder(permissions); case VIEWTYPE_VERSIONS: View versions = inflater.inflate(R.layout.app_details2_links, parent, false); return new VersionsViewHolder(versions); case VIEWTYPE_VERSION: View version = inflater.inflate(R.layout.apklistitem, parent, false); return new VersionViewHolder(version); } return null; } @Override public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) { int viewType = getItemViewType(position); switch (viewType) { case VIEWTYPE_HEADER: HeaderViewHolder header = (HeaderViewHolder) holder; headerView = header; header.bindModel(); break; case VIEWTYPE_SCREENSHOTS: ((ScreenShotsViewHolder) holder).bindModel(); break; case VIEWTYPE_WHATS_NEW: ((WhatsNewViewHolder) holder).bindModel(); break; case VIEWTYPE_DONATE: ((DonateViewHolder) holder).bindModel(); break; case VIEWTYPE_LINKS: ((LinksViewHolder) holder).bindModel(); break; case VIEWTYPE_PERMISSIONS: ((PermissionsViewHolder) holder).bindModel(); break; case VIEWTYPE_VERSIONS: ((VersionsViewHolder) holder).bindModel(); break; case VIEWTYPE_VERSION: final Apk apk = (Apk) items.get(position); ((VersionViewHolder) holder).bindModel(apk); break; } } @Override public void onViewRecycled(RecyclerView.ViewHolder holder) { if (holder instanceof HeaderViewHolder) { headerView = null; } super.onViewRecycled(holder); } @Override public int getItemCount() { return items.size(); } @Override public int getItemViewType(int position) { if (items.get(position) instanceof Apk) { return VIEWTYPE_VERSION; } return (Integer) items.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 View buttonLayout; final Button buttonPrimaryView; final Button buttonSecondaryView; final View progressLayout; final ProgressBar progressBar; final TextView progressLabel; final TextView progressPercent; final View progressCancel; 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); buttonLayout = view.findViewById(R.id.button_layout); buttonPrimaryView = (Button) view.findViewById(R.id.primaryButtonView); buttonSecondaryView = (Button) view.findViewById(R.id.secondaryButtonView); progressLayout = view.findViewById(R.id.progress_layout); progressBar = (ProgressBar) view.findViewById(R.id.progress_bar); progressLabel = (TextView) view.findViewById(R.id.progress_label); progressPercent = (TextView) view.findViewById(R.id.progress_percent); progressCancel = view.findViewById(R.id.progress_cancel); 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) { // Make this "header section" the focused child, so that RecyclerView will use // it as the anchor in the layout process. Otherwise the RV might select another // view as the anchor, resulting in that the top of this view is instead scrolled // off the screen. Refer to LinearLayoutManager.updateAnchorFromChildren(...). recyclerView.requestChildFocus(itemView, itemView); 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); } } }); // 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); } public void setProgress(int bytesDownloaded, int totalBytes, int resIdString) { if (bytesDownloaded == 0 && totalBytes == 0) { // Remove progress bar progressLayout.setVisibility(View.GONE); buttonLayout.setVisibility(View.VISIBLE); } else { progressBar.setMax(totalBytes); progressBar.setProgress(bytesDownloaded); progressBar.setIndeterminate(totalBytes == -1); if (resIdString != 0) { progressLabel.setText(resIdString); progressPercent.setText(""); } else if (totalBytes > 0 && bytesDownloaded >= 0) { float percent = bytesDownloaded * 100 / totalBytes; progressLabel.setText(Utils.getFriendlySize(bytesDownloaded) + " / " + Utils.getFriendlySize(totalBytes)); NumberFormat format = NumberFormat.getPercentInstance(); format.setMaximumFractionDigits(0); progressPercent.setText(format.format(percent / 100)); } else if (bytesDownloaded >= 0) { progressLabel.setText(Utils.getFriendlySize(bytesDownloaded)); progressPercent.setText(""); } // Make sure it's visible if (progressLayout.getVisibility() != View.VISIBLE) { progressLayout.setVisibility(View.VISIBLE); buttonLayout.setVisibility(View.GONE); } } } public void bindModel() { ImageLoader.getInstance().displayImage(app.iconUrlLarge, iconView, displayImageOptions); titleView.setText(app.name); if (!TextUtils.isEmpty(app.author)) { authorView.setText(context.getString(R.string.by_author) + " " + app.author); authorView.setVisibility(View.VISIBLE); } else { authorView.setVisibility(View.GONE); } summaryView.setText(app.summary); final Spanned desc = Html.fromHtml(app.description, null, new Utils.HtmlTagHandler()); descriptionView.setMovementMethod(LinkMovementMethod.getInstance()); descriptionView.setText(trimTrailingNewlines(desc)); if (descriptionView.getText() instanceof Spannable) { Spannable spannable = (Spannable) descriptionView.getText(); URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class); for (URLSpan span : spans) { int start = spannable.getSpanStart(span); int end = spannable.getSpanEnd(span); int flags = spannable.getSpanFlags(span); spannable.removeSpan(span); // Create out own safe link span SafeURLSpan safeUrlSpan = new SafeURLSpan(span.getURL()); spannable.setSpan(safeUrlSpan, start, end, flags); } } descriptionView.post(new Runnable() { @Override public void run() { if (descriptionView.getLineCount() <= HeaderViewHolder.MAX_LINES) { descriptionMoreView.setVisibility(View.GONE); } else { descriptionMoreView.setVisibility(View.VISIBLE); } } }); buttonSecondaryView.setText(R.string.menu_uninstall); buttonSecondaryView.setVisibility(app.isInstalled() ? View.VISIBLE : View.INVISIBLE); buttonSecondaryView.setOnClickListener(onUnInstallClickListener); buttonPrimaryView.setText(R.string.menu_install); buttonPrimaryView.setVisibility(versions.size() > 0 ? View.VISIBLE : View.GONE); if (callbacks.isAppDownloading()) { buttonPrimaryView.setText(R.string.downloading); buttonPrimaryView.setEnabled(false); } else if (!app.isInstalled() && app.suggestedVersionCode > 0 && versions.size() > 0) { // Check count > 0 due to incompatible apps resulting in an empty list. callbacks.disableAndroidBeam(); // Set Install button and hide second button buttonPrimaryView.setText(R.string.menu_install); buttonPrimaryView.setOnClickListener(onInstallClickListener); buttonPrimaryView.setEnabled(true); } else if (app.isInstalled()) { callbacks.enableAndroidBeam(); if (app.canAndWantToUpdate(context)) { buttonPrimaryView.setText(R.string.menu_upgrade); buttonPrimaryView.setOnClickListener(onUpgradeClickListener); } else { if (context.getPackageManager().getLaunchIntentForPackage(app.packageName) != null) { buttonPrimaryView.setText(R.string.menu_launch); buttonPrimaryView.setOnClickListener(onLaunchClickListener); } else { buttonPrimaryView.setVisibility(View.GONE); } } buttonPrimaryView.setEnabled(true); } if (callbacks.isAppDownloading()) { buttonLayout.setVisibility(View.GONE); progressLayout.setVisibility(View.VISIBLE); } else { buttonLayout.setVisibility(View.VISIBLE); progressLayout.setVisibility(View.GONE); } progressCancel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { callbacks.installCancel(); } }); } } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); this.recyclerView = recyclerView; } @Override public void onDetachedFromRecyclerView(RecyclerView recyclerView) { this.recyclerView = null; super.onDetachedFromRecyclerView(recyclerView); } private class ScreenShotsViewHolder extends RecyclerView.ViewHolder { final RecyclerView recyclerView; LinearLayoutManagerSnapHelper snapHelper; ScreenShotsViewHolder(View view) { super(view); recyclerView = (RecyclerView) view.findViewById(R.id.screenshots); } public void bindModel() { LinearLayoutManager lm = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false); recyclerView.setLayoutManager(lm); ScreenShotsRecyclerViewAdapter adapter = new ScreenShotsRecyclerViewAdapter(itemView.getContext(), app); recyclerView.setAdapter(adapter); recyclerView.setHasFixedSize(true); recyclerView.setNestedScrollingEnabled(false); if (snapHelper != null) { snapHelper.attachToRecyclerView(null); } snapHelper = new LinearLayoutManagerSnapHelper(lm); snapHelper.setLinearSnapHelperListener(adapter); snapHelper.attachToRecyclerView(recyclerView); } } 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 textView; final LinearLayout contentView; DonateViewHolder(View view) { super(view); textView = (TextView) view.findViewById(R.id.information); contentView = (LinearLayout) view.findViewById(R.id.ll_information); } public void bindModel() { contentView.removeAllViews(); // Donate button if (uriIsSetAndCanBeOpened(app.donateURL)) { addLinkItemView(contentView, R.string.menu_donate, R.drawable.ic_donate, app.donateURL); } // Bitcoin if (uriIsSetAndCanBeOpened(app.getBitcoinUri())) { addLinkItemView(contentView, R.string.menu_bitcoin, R.drawable.ic_bitcoin, app.getBitcoinUri()); } // Litecoin if (uriIsSetAndCanBeOpened(app.getLitecoinUri())) { addLinkItemView(contentView, R.string.menu_litecoin, R.drawable.ic_litecoin, app.getLitecoinUri()); } // Flattr if (uriIsSetAndCanBeOpened(app.getFlattrUri())) { addLinkItemView(contentView, R.string.menu_flattr, R.drawable.ic_flattr, app.getFlattrUri()); } } } private abstract class ExpandableLinearLayoutViewHolder extends RecyclerView.ViewHolder { final TextView headerView; final LinearLayout contentView; ExpandableLinearLayoutViewHolder(View view) { super(view); headerView = (TextView) view.findViewById(R.id.information); contentView = (LinearLayout) view.findViewById(R.id.ll_content); } } private class VersionsViewHolder extends ExpandableLinearLayoutViewHolder { VersionsViewHolder(View view) { super(view); } public void bindModel() { itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setShowVersions(!showVersions); } }); headerView.setText(R.string.versions); TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(headerView, R.drawable.ic_access_time_24dp_grey600, 0, showVersions ? R.drawable.ic_expand_less_grey600 : R.drawable.ic_expand_more_grey600, 0); } } private class PermissionsViewHolder extends ExpandableLinearLayoutViewHolder { PermissionsViewHolder(View view) { super(view); } public void bindModel() { itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { boolean shouldBeVisible = contentView.getVisibility() != View.VISIBLE; contentView.setVisibility(shouldBeVisible ? View.VISIBLE : View.GONE); TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(headerView, R.drawable.ic_lock_24dp_grey600, 0, shouldBeVisible ? R.drawable.ic_expand_less_grey600 : R.drawable.ic_expand_more_grey600, 0); } }); headerView.setText(R.string.permissions); TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(headerView, R.drawable.ic_lock_24dp_grey600, 0, R.drawable.ic_expand_more_grey600, 0); contentView.removeAllViews(); AppDiff appDiff = new AppDiff(context.getPackageManager(), versions.get(0)); AppSecurityPermissions perms = new AppSecurityPermissions(context, appDiff.pkgInfo); contentView.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL)); } } private class LinksViewHolder extends ExpandableLinearLayoutViewHolder { LinksViewHolder(View view) { super(view); } public void bindModel() { itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { boolean shouldBeVisible = contentView.getVisibility() != View.VISIBLE; contentView.setVisibility(shouldBeVisible ? View.VISIBLE : View.GONE); TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(headerView, R.drawable.ic_website, 0, shouldBeVisible ? R.drawable.ic_expand_less_grey600 : R.drawable.ic_expand_more_grey600, 0); } }); headerView.setText(R.string.links); TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(headerView, R.drawable.ic_website, 0, R.drawable.ic_expand_more_grey600, 0); contentView.removeAllViews(); // Source button if (uriIsSetAndCanBeOpened(app.sourceURL)) { addLinkItemView(contentView, R.string.menu_source, R.drawable.ic_source_code, app.sourceURL); } // Issues button if (uriIsSetAndCanBeOpened(app.trackerURL)) { addLinkItemView(contentView, R.string.menu_issues, R.drawable.ic_issues, app.trackerURL); } // Changelog button if (uriIsSetAndCanBeOpened(app.changelogURL)) { addLinkItemView(contentView, R.string.menu_changelog, R.drawable.ic_changelog, app.changelogURL); } // Website button if (uriIsSetAndCanBeOpened(app.webURL)) { addLinkItemView(contentView, R.string.menu_website, R.drawable.ic_website, app.webURL); } // Email button final String subject = Uri.encode(context.getString(R.string.app_details_subject, app.name)); String emailUrl = app.email == null ? null : ("mailto:" + app.email + "?subject=" + subject); if (uriIsSetAndCanBeOpened(emailUrl)) { addLinkItemView(contentView, R.string.menu_email, R.drawable.ic_email, emailUrl); } } } private class VersionViewHolder extends RecyclerView.ViewHolder { final TextView version; final TextView status; final TextView repository; final TextView size; final TextView api; final TextView incompatibleReasons; final TextView buildtype; final TextView added; final TextView nativecode; VersionViewHolder(View view) { super(view); version = (TextView) view.findViewById(R.id.version); status = (TextView) view.findViewById(R.id.status); repository = (TextView) view.findViewById(R.id.repository); size = (TextView) view.findViewById(R.id.size); api = (TextView) view.findViewById(R.id.api); incompatibleReasons = (TextView) view.findViewById(R.id.incompatible_reasons); buildtype = (TextView) view.findViewById(R.id.buildtype); added = (TextView) view.findViewById(R.id.added); nativecode = (TextView) view.findViewById(R.id.nativecode); int margin = context.getResources().getDimensionPixelSize(R.dimen.layout_horizontal_margin); int padding = context.getResources().getDimensionPixelSize(R.dimen.details_activity_padding); ViewCompat.setPaddingRelative(view, margin + padding + ViewCompat.getPaddingStart(view), view.getPaddingTop(), ViewCompat.getPaddingEnd(view), view.getPaddingBottom()); } public void bindModel(final Apk apk) { java.text.DateFormat df = DateFormat.getDateFormat(context); version.setText(context.getString(R.string.version) + " " + apk.versionName + (apk.versionCode == app.suggestedVersionCode ? " ☆" : "")); status.setText(getInstalledStatus(apk)); repository.setText(context.getString(R.string.repo_provider, RepoProvider.Helper.findById(context, apk.repo).getName())); if (apk.size > 0) { size.setText(Utils.getFriendlySize(apk.size)); size.setVisibility(View.VISIBLE); } else { size.setVisibility(View.GONE); } if (!Preferences.get().expertMode()) { api.setVisibility(View.GONE); } else if (apk.minSdkVersion > 0 && apk.maxSdkVersion < Apk.SDK_VERSION_MAX_VALUE) { api.setText(context.getString(R.string.minsdk_up_to_maxsdk, Utils.getAndroidVersionName(apk.minSdkVersion), Utils.getAndroidVersionName(apk.maxSdkVersion))); api.setVisibility(View.VISIBLE); } else if (apk.minSdkVersion > 0) { api.setText(context.getString(R.string.minsdk_or_later, Utils.getAndroidVersionName(apk.minSdkVersion))); api.setVisibility(View.VISIBLE); } else if (apk.maxSdkVersion > 0) { api.setText(context.getString(R.string.up_to_maxsdk, Utils.getAndroidVersionName(apk.maxSdkVersion))); api.setVisibility(View.VISIBLE); } if (apk.srcname != null) { buildtype.setText("source"); } else { buildtype.setText("bin"); } if (apk.added != null) { added.setText(context.getString(R.string.added_on, df.format(apk.added))); added.setVisibility(View.VISIBLE); } else { added.setVisibility(View.GONE); } if (Preferences.get().expertMode() && apk.nativecode != null) { nativecode.setText(TextUtils.join(" ", apk.nativecode)); nativecode.setVisibility(View.VISIBLE); } else { nativecode.setVisibility(View.GONE); } if (apk.incompatibleReasons != null) { incompatibleReasons.setText( context.getResources().getString( R.string.requires_features, TextUtils.join(", ", apk.incompatibleReasons))); incompatibleReasons.setVisibility(View.VISIBLE); } else { incompatibleReasons.setVisibility(View.GONE); } // Disable it all if it isn't compatible... final View[] views = { itemView, version, status, repository, size, api, buildtype, added, nativecode, }; for (final View v : views) { v.setEnabled(apk.compatible); } itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { callbacks.installApk(apk); } }); } } private void addLinkItemView(ViewGroup parent, int resIdText, int resIdDrawable, final String url) { TextView view = (TextView) LayoutInflater.from(parent.getContext()).inflate(R.layout.app_details2_link_item, parent, false); view.setText(resIdText); TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(view, resIdDrawable, 0, 0, 0); parent.addView(view); view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onLinkClicked(url); } }); } private String getInstalledStatus(final Apk apk) { // Definitely not installed. if (apk.versionCode != app.installedVersionCode) { return context.getString(R.string.app_not_installed); } // Definitely installed this version. if (apk.sig != null && apk.sig.equals(app.installedSig)) { return context.getString(R.string.app_installed); } // Installed the same version, but from someplace else. final String installerPkgName; try { installerPkgName = context.getPackageManager().getInstallerPackageName(app.packageName); } catch (IllegalArgumentException e) { Log.w("AppDetailsAdapter", "Application " + app.packageName + " is not installed anymore"); return context.getString(R.string.app_not_installed); } if (TextUtils.isEmpty(installerPkgName)) { return context.getString(R.string.app_inst_unknown_source); } final String installerLabel = InstalledAppProvider .getApplicationLabel(context, installerPkgName); return context.getString(R.string.app_inst_known_source, installerLabel); } private void onLinkClicked(String url) { if (!TextUtils.isEmpty(url)) { callbacks.openUrl(url); } } private final View.OnClickListener onInstallClickListener = new View.OnClickListener() { @Override public void onClick(View v) { callbacks.installApk(); } }; private final View.OnClickListener onUnInstallClickListener = new View.OnClickListener() { @Override public void onClick(View v) { callbacks.uninstallApk(); } }; private final View.OnClickListener onUpgradeClickListener = new View.OnClickListener() { @Override public void onClick(View v) { callbacks.upgradeApk(); } }; private final View.OnClickListener onLaunchClickListener = new View.OnClickListener() { @Override public void onClick(View v) { callbacks.launchApk(); } }; private boolean uriIsSetAndCanBeOpened(String s) { if (TextUtils.isEmpty(s)) { return false; } Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(s)); return intent.resolveActivity(context.getPackageManager()) != null; } /** * 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). */ public static CharSequence trimTrailingNewlines(CharSequence s) { if (TextUtils.isEmpty(s)) { 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); } @SuppressLint("ParcelCreator") private static final class SafeURLSpan extends URLSpan { SafeURLSpan(String url) { super(url); } @Override public void onClick(View widget) { try { super.onClick(widget); } catch (ActivityNotFoundException ex) { Toast.makeText(widget.getContext(), widget.getContext().getString(R.string.no_handler_app, getURL()), Toast.LENGTH_LONG).show(); } } } }