diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java index d268d521c..f6fbae4a6 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java @@ -1,10 +1,8 @@ package org.fdroid.fdroid; -import android.annotation.SuppressLint; import android.app.Activity; import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; -import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; @@ -16,32 +14,17 @@ import android.net.Uri; import android.os.Bundle; import android.support.design.widget.CoordinatorLayout; import android.support.v4.content.LocalBroadcastManager; -import android.support.v4.widget.TextViewCompat; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatDelegate; -import android.support.v7.text.AllCapsTransformationMethod; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; -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.Menu; import android.view.MenuItem; -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; @@ -52,8 +35,6 @@ import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppProvider; -import org.fdroid.fdroid.data.InstalledAppProvider; -import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.installer.Installer; @@ -61,19 +42,10 @@ import org.fdroid.fdroid.installer.InstallerFactory; import org.fdroid.fdroid.installer.InstallerService; import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.DownloaderService; -import org.fdroid.fdroid.privileged.views.AppDiff; -import org.fdroid.fdroid.privileged.views.AppSecurityPermissions; -import org.fdroid.fdroid.views.LinearLayoutManagerSnapHelper; -import org.fdroid.fdroid.views.ScreenShotsRecyclerViewAdapter; +import org.fdroid.fdroid.views.AppDetailsRecyclerViewAdapter; import org.fdroid.fdroid.views.ShareChooserDialog; -import java.text.NumberFormat; -import java.util.ArrayList; -import java.util.List; - -import static android.support.v7.widget.RecyclerView.NO_POSITION; - -public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog.ShareChooserDialogListener { +public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog.ShareChooserDialogListener, AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks { static { AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); } @@ -110,7 +82,7 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog mLocalBroadcastManager = LocalBroadcastManager.getInstance(this); mRecyclerView = (RecyclerView) findViewById(R.id.rvDetails); - mAdapter = new AppDetailsRecyclerViewAdapter(this); + mAdapter = new AppDetailsRecyclerViewAdapter(this, mApp, this); LinearLayoutManager lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); lm.setStackFromEnd(false); mRecyclerView.setLayoutManager(lm); @@ -209,770 +181,6 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog startActivity(shareIntent); } - public class AppDetailsRecyclerViewAdapter - extends RecyclerView.Adapter { - - private final int VIEWTYPE_HEADER = 0; - private final int VIEWTYPE_SCREENSHOTS = 1; - private final int VIEWTYPE_WHATS_NEW = 2; - private final int VIEWTYPE_DONATE = 3; - private final int VIEWTYPE_LINKS = 4; - private final int VIEWTYPE_PERMISSIONS = 5; - private final int VIEWTYPE_VERSIONS = 6; - private final int VIEWTYPE_VERSION = 7; - - private final Context mContext; - private ArrayList mItems; - private ArrayList mVersions; - private boolean mShowVersions; - - private HeaderViewHolder mHeaderView; - - public AppDetailsRecyclerViewAdapter(Context context) { - mContext = context; - updateItems(); - } - - public void updateItems() { - - // Get versions - mVersions = new ArrayList<>(); - final List apks = ApkProvider.Helper.findByPackageName(mContext, mApp.packageName); - for (final Apk apk : apks) { - if (apk.compatible || Preferences.get().showIncompatibleVersions()) { - mVersions.add(apk); - } - } - - if (mItems == null) - mItems = new ArrayList<>(); - else - mItems.clear(); - addItem(VIEWTYPE_HEADER); - addItem(VIEWTYPE_SCREENSHOTS); - addItem(VIEWTYPE_WHATS_NEW); - addItem(VIEWTYPE_DONATE); - addItem(VIEWTYPE_LINKS); - addItem(VIEWTYPE_PERMISSIONS); - addItem(VIEWTYPE_VERSIONS); - } - - public void setShowVersions(boolean showVersions) { - mShowVersions = showVersions; - mItems.removeAll(mVersions); - if (showVersions) { - mItems.addAll(mItems.indexOf(VIEWTYPE_VERSIONS) + 1, mVersions); - } - 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) { - if (!shouldShowDonate()) - return; - } else if (item == VIEWTYPE_PERMISSIONS) { - if (!shouldShowPermissions()) - return; - } - mItems.add(item); - } - - private boolean shouldShowPermissions() { - // Figure out if we should show permissions section - Apk curApk = null; - for (int i = 0; i < mVersions.size(); i++) { - final Apk apk = mVersions.get(i); - if (apk.versionCode == mApp.suggestedVersionCode) { - curApk = apk; - break; - } - } - final boolean curApkCompatible = curApk != null && curApk.compatible; - if (mVersions.size() > 0 && (curApkCompatible || Preferences.get().showIncompatibleVersions())) { - return true; - } - return false; - } - - private boolean shouldShowDonate() { - return uriIsSetAndCanBeOpened(mApp.donateURL) || - uriIsSetAndCanBeOpened(mApp.bitcoinAddr) || - uriIsSetAndCanBeOpened(mApp.litecoinAddr) || - uriIsSetAndCanBeOpened(mApp.flattrID); - } - - public HeaderViewHolder getHeaderView() { - return mHeaderView; - } - - @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); - } else if (viewType == VIEWTYPE_DONATE) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.app_details2_donate, parent, false); - return new DonateViewHolder(view); - } else if (viewType == VIEWTYPE_LINKS) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.app_details2_links, parent, false); - return new ExpandableLinearLayoutViewHolder(view); - } else if (viewType == VIEWTYPE_PERMISSIONS) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.app_details2_links, parent, false); - return new ExpandableLinearLayoutViewHolder(view); - } else if (viewType == VIEWTYPE_VERSIONS) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.app_details2_links, parent, false); - return new ExpandableLinearLayoutViewHolder(view); - } else if (viewType == VIEWTYPE_VERSION) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.apklistitem, parent, false); - return new VersionViewHolder(view); - } - return null; - } - - @Override - public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) { - int viewType = getItemViewType(position); - if (viewType == VIEWTYPE_HEADER) { - final HeaderViewHolder vh = (HeaderViewHolder) holder; - mHeaderView = vh; - 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(LinkMovementMethod.getInstance()); - vh.descriptionView.setText(trimNewlines(desc)); - if (vh.descriptionView.getText() instanceof Spannable) { - Spannable spannable = (Spannable) vh.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); - } - } - 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(isAppInstalled() ? View.VISIBLE : View.INVISIBLE); - vh.buttonSecondaryView.setOnClickListener(mOnUnInstallClickListener); - vh.buttonPrimaryView.setText(R.string.menu_install); - vh.buttonPrimaryView.setVisibility(mVersions.size() > 0 ? View.VISIBLE : View.GONE); - if (mActiveDownloadUrlString != null) { - vh.buttonPrimaryView.setText(R.string.downloading); - vh.buttonPrimaryView.setEnabled(false); - } else if (!isAppInstalled() && mApp.suggestedVersionCode > 0 && - mVersions.size() > 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(AppDetails2.this); - // Set Install button and hide second button - vh.buttonPrimaryView.setText(R.string.menu_install); - vh.buttonPrimaryView.setOnClickListener(mOnInstallClickListener); - vh.buttonPrimaryView.setEnabled(true); - } else if (isAppInstalled()) { - // If App is installed - //installed = true; - //statusView.setText(getString(R.string.details_installed, app.installedVersionName)); - NfcHelper.setAndroidBeam(AppDetails2.this, mApp.packageName); - if (mApp.canAndWantToUpdate(AppDetails2.this)) { - //updateWanted = true; - vh.buttonPrimaryView.setText(R.string.menu_upgrade); - vh.buttonPrimaryView.setOnClickListener(mOnUpgradeClickListener); - } else { - //updateWanted = false; - if (getPackageManager().getLaunchIntentForPackage(mApp.packageName) != null) { - vh.buttonPrimaryView.setText(R.string.menu_launch); - vh.buttonPrimaryView.setOnClickListener(mOnLaunchClickListener); - } else { - vh.buttonPrimaryView.setVisibility(View.GONE); - //vh.buttonPrimaryView.setText(R.string.menu_uninstall); - } - } - vh.buttonPrimaryView.setEnabled(true); - } - if (!TextUtils.isEmpty(mActiveDownloadUrlString)) { - vh.buttonLayout.setVisibility(View.GONE); - vh.progressLayout.setVisibility(View.VISIBLE); - } else { - vh.buttonLayout.setVisibility(View.VISIBLE); - vh.progressLayout.setVisibility(View.GONE); - } - vh.progressCancel.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - cancelInstall(); - } - }); - - /*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(vh.itemView.getContext(), mApp); - vh.recyclerView.setAdapter(adapter); - vh.recyclerView.setHasFixedSize(true); - vh.recyclerView.setNestedScrollingEnabled(false); - if (vh.snapHelper != null) - vh.snapHelper.attachToRecyclerView(null); - vh.snapHelper = new LinearLayoutManagerSnapHelper(lm); - vh.snapHelper.setLinearSnapHelperListener(adapter); - vh.snapHelper.attachToRecyclerView(vh.recyclerView); - } else if (viewType == VIEWTYPE_WHATS_NEW) { - WhatsNewViewHolder vh = (WhatsNewViewHolder) holder; - vh.textView.setText("WHATS NEW GOES HERE"); - } else if (viewType == VIEWTYPE_DONATE) { - DonateViewHolder vh = (DonateViewHolder) holder; - vh.contentView.removeAllViews(); - - // Donate button - if (uriIsSetAndCanBeOpened(mApp.donateURL)) { - addLinkItemView(vh.contentView, R.string.menu_donate, R.drawable.ic_donate, mApp.donateURL); - } - - // Bitcoin - if (uriIsSetAndCanBeOpened(mApp.bitcoinAddr)) { - addLinkItemView(vh.contentView, R.string.menu_bitcoin, R.drawable.ic_bitcoin, "bitcoin:" + mApp.bitcoinAddr); - } - - // Litecoin - if (uriIsSetAndCanBeOpened(mApp.litecoinAddr)) { - addLinkItemView(vh.contentView, R.string.menu_litecoin, R.drawable.ic_litecoin, "litecoin:" + mApp.litecoinAddr); - } - - // Flattr - if (uriIsSetAndCanBeOpened(mApp.flattrID)) { - addLinkItemView(vh.contentView, R.string.menu_flattr, R.drawable.ic_flattr, "https://flattr.com/thing/" + mApp.flattrID); - } - } else if (viewType == VIEWTYPE_LINKS) { - final ExpandableLinearLayoutViewHolder vh = (ExpandableLinearLayoutViewHolder) holder; - vh.itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - boolean shouldBeVisible = (vh.contentView.getVisibility() != View.VISIBLE); - vh.contentView.setVisibility(shouldBeVisible ? View.VISIBLE : View.GONE); - TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(vh.headerView, R.drawable.ic_website, 0, shouldBeVisible ? R.drawable.ic_expand_less_grey600 : R.drawable.ic_expand_more_grey600, 0); - } - }); - vh.headerView.setText(R.string.links); - TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(vh.headerView, R.drawable.ic_website, 0, R.drawable.ic_expand_more_grey600, 0); - vh.contentView.removeAllViews(); - - // Source button - if (uriIsSetAndCanBeOpened(mApp.sourceURL)) { - addLinkItemView(vh.contentView, R.string.menu_source, R.drawable.ic_source_code, mApp.sourceURL); - } - - // Issues button - if (uriIsSetAndCanBeOpened(mApp.trackerURL)) { - addLinkItemView(vh.contentView, R.string.menu_issues, R.drawable.ic_issues, mApp.trackerURL); - } - - // Changelog button - if (uriIsSetAndCanBeOpened(mApp.changelogURL)) { - addLinkItemView(vh.contentView, R.string.menu_changelog, R.drawable.ic_changelog, mApp.changelogURL); - } - - // Website button - if (uriIsSetAndCanBeOpened(mApp.webURL)) { - addLinkItemView(vh.contentView, R.string.menu_website, R.drawable.ic_website, mApp.webURL); - } - - // Email button - if (uriIsSetAndCanBeOpened(mApp.email)) { - final String subject = Uri.encode(getString(R.string.app_details_subject, mApp.name)); - String url = "mailto:" + mApp.email + "?subject=" + subject; - addLinkItemView(vh.contentView, R.string.menu_email, R.drawable.ic_email, url); - } - } else if (viewType == VIEWTYPE_PERMISSIONS) { - final ExpandableLinearLayoutViewHolder vh = (ExpandableLinearLayoutViewHolder) holder; - vh.itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - boolean shouldBeVisible = (vh.contentView.getVisibility() != View.VISIBLE); - vh.contentView.setVisibility(shouldBeVisible ? View.VISIBLE : View.GONE); - TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(vh.headerView, R.drawable.ic_lock_24dp_grey600, 0, shouldBeVisible ? R.drawable.ic_expand_less_grey600 : R.drawable.ic_expand_more_grey600, 0); - } - }); - vh.headerView.setText(R.string.permissions); - TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(vh.headerView, R.drawable.ic_lock_24dp_grey600, 0, R.drawable.ic_expand_more_grey600, 0); - vh.contentView.removeAllViews(); - AppDiff appDiff = new AppDiff(getPackageManager(), mVersions.get(0)); - AppSecurityPermissions perms = new AppSecurityPermissions(mContext, appDiff.pkgInfo); - vh.contentView.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL)); - } else if (viewType == VIEWTYPE_VERSIONS) { - final ExpandableLinearLayoutViewHolder vh = (ExpandableLinearLayoutViewHolder) holder; - vh.itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - setShowVersions(!mShowVersions); - } - }); - vh.headerView.setText(R.string.versions); - TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(vh.headerView, R.drawable.ic_access_time_24dp_grey600, 0, mShowVersions ? R.drawable.ic_expand_less_grey600 : R.drawable.ic_expand_more_grey600, 0); - } else if (viewType == VIEWTYPE_VERSION) { - final VersionViewHolder vh = (VersionViewHolder) holder; - java.text.DateFormat df = DateFormat.getDateFormat(mContext); - final Apk apk = (Apk)mItems.get(position); - - vh.version.setText(mContext.getString(R.string.version) - + " " + apk.versionName - + (apk.versionCode == mApp.suggestedVersionCode ? " ☆" : "")); - - vh.status.setText(getInstalledStatus(apk)); - - vh.repository.setText(mContext.getString(R.string.repo_provider, - RepoProvider.Helper.findById(mContext, apk.repo).getName())); - - if (apk.size > 0) { - vh.size.setText(Utils.getFriendlySize(apk.size)); - vh.size.setVisibility(View.VISIBLE); - } else { - vh.size.setVisibility(View.GONE); - } - - if (!Preferences.get().expertMode()) { - vh.api.setVisibility(View.GONE); - } else if (apk.minSdkVersion > 0 && apk.maxSdkVersion < Apk.SDK_VERSION_MAX_VALUE) { - vh.api.setText(mContext.getString(R.string.minsdk_up_to_maxsdk, - Utils.getAndroidVersionName(apk.minSdkVersion), - Utils.getAndroidVersionName(apk.maxSdkVersion))); - vh.api.setVisibility(View.VISIBLE); - } else if (apk.minSdkVersion > 0) { - vh.api.setText(mContext.getString(R.string.minsdk_or_later, - Utils.getAndroidVersionName(apk.minSdkVersion))); - vh.api.setVisibility(View.VISIBLE); - } else if (apk.maxSdkVersion > 0) { - vh.api.setText(mContext.getString(R.string.up_to_maxsdk, - Utils.getAndroidVersionName(apk.maxSdkVersion))); - vh.api.setVisibility(View.VISIBLE); - } - - if (apk.srcname != null) { - vh.buildtype.setText("source"); - } else { - vh.buildtype.setText("bin"); - } - - if (apk.added != null) { - vh.added.setText(mContext.getString(R.string.added_on, - df.format(apk.added))); - vh.added.setVisibility(View.VISIBLE); - } else { - vh.added.setVisibility(View.GONE); - } - - if (Preferences.get().expertMode() && apk.nativecode != null) { - vh.nativecode.setText(TextUtils.join(" ", apk.nativecode)); - vh.nativecode.setVisibility(View.VISIBLE); - } else { - vh.nativecode.setVisibility(View.GONE); - } - - if (apk.incompatibleReasons != null) { - vh.incompatibleReasons.setText( - mContext.getResources().getString( - R.string.requires_features, - TextUtils.join(", ", apk.incompatibleReasons))); - vh.incompatibleReasons.setVisibility(View.VISIBLE); - } else { - vh.incompatibleReasons.setVisibility(View.GONE); - } - - // Disable it all if it isn't compatible... - final View[] views = { - vh.itemView, - vh.version, - vh.status, - vh.repository, - vh.size, - vh.api, - vh.buildtype, - vh.added, - vh.nativecode, - }; - for (final View v : views) { - v.setEnabled(apk.compatible); - } - } - } - - @Override - public void onViewRecycled(RecyclerView.ViewHolder holder) { - if (holder instanceof HeaderViewHolder) { - mHeaderView = null; - } - super.onViewRecycled(holder); - } - - @Override - public int getItemCount() { - return mItems.size(); - } - - @Override - public int getItemViewType(int position) { - if (mItems.get(position) instanceof Apk) - return VIEWTYPE_VERSION; - return (Integer)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 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) { - // 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 firstChild = mRecyclerView.getChildAt(0); - posOffset = (firstChild == null) ? 0 : (firstChild.getTop()); // - mRecyclerView.getPaddingTop()); - } - 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); - } - - 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 class ScreenShotsViewHolder extends RecyclerView.ViewHolder { - final RecyclerView recyclerView; - LinearLayoutManagerSnapHelper snapHelper; - - ScreenShotsViewHolder(View view) { - super(view); - recyclerView = (RecyclerView) view.findViewById(R.id.screenshots); - } - } - - public class WhatsNewViewHolder extends RecyclerView.ViewHolder { - final TextView textView; - - WhatsNewViewHolder(View view) { - super(view); - textView = (TextView) view.findViewById(R.id.text); - } - } - - public class DonateViewHolder extends RecyclerView.ViewHolder { - final TextView textView; - final LinearLayout contentView; - - DonateViewHolder(View view) { - super(view); - textView = (TextView) view.findViewById(R.id.text); - contentView = (LinearLayout) view.findViewById(R.id.ll_information); - } - } - - public 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); - } - } - - public 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); - } - } - - private void addLinkItemView(ViewGroup parent, int resIdText, int resIdDrawable, String url) { - TextView view = (TextView) LayoutInflater.from(parent.getContext()).inflate(R.layout.app_details2_link_item, parent, false); - view.setTag(url); - 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((String) v.getTag()); - } - }); - } - - private String getInstalledStatus(final Apk apk) { - // Definitely not installed. - if (apk.versionCode != mApp.installedVersionCode) { - return mContext.getString(R.string.app_not_installed); - } - // Definitely installed this version. - if (apk.sig != null && apk.sig.equals(mApp.installedSig)) { - return mContext.getString(R.string.app_installed); - } - // Installed the same version, but from someplace else. - final String installerPkgName; - try { - installerPkgName = mContext.getPackageManager().getInstallerPackageName(mApp.packageName); - } catch (IllegalArgumentException e) { - Log.w(TAG, "Application " + mApp.packageName + " is not installed anymore"); - return mContext.getString(R.string.app_not_installed); - } - if (TextUtils.isEmpty(installerPkgName)) { - return mContext.getString(R.string.app_inst_unknown_source); - } - final String installerLabel = InstalledAppProvider - .getApplicationLabel(mContext, installerPkgName); - return mContext.getString(R.string.app_inst_known_source, installerLabel); - } - - private void onLinkClicked(String url) { - if (!TextUtils.isEmpty(url)) { - tryOpenUri(url); - } - } - - private View.OnClickListener mOnInstallClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - Apk apkToInstall = ApkProvider.Helper.findApkFromAnyRepo(AppDetails2.this, mApp.packageName, mApp.suggestedVersionCode); - - // If not installed, install - //btMain.setEnabled(false); - //btMain.setText(R.string.system_install_installing); - - installApk(apkToInstall); - } - }; - - private View.OnClickListener mOnUnInstallClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - uninstallApk(); - } - }; - - private View.OnClickListener mOnUpgradeClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - Apk apkToInstall = ApkProvider.Helper.findApkFromAnyRepo(AppDetails2.this, mApp.packageName, mApp.suggestedVersionCode); - installApk(apkToInstall); - } - }; - - private View.OnClickListener mOnLaunchClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - launchApk(mApp.packageName); - } - }; - } - - private boolean uriIsSetAndCanBeOpened(String s) { - if (TextUtils.isEmpty(s)) - return false; - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(s)); - return (intent.resolveActivity(getPackageManager()) != null); - } - - private void tryOpenUri(String s) { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(s)); - if (intent.resolveActivity(getPackageManager()) == null) { - Toast.makeText(this, - getString(R.string.no_handler_app, intent.getDataString()), - Toast.LENGTH_LONG).show(); - return; - } - startActivity(intent); - } - - // 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); - } - - @SuppressLint("ParcelCreator") - private static final class SafeURLSpan extends URLSpan { - public 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(); - } - } - } - @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { @@ -995,7 +203,8 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog } // Install the version of this app denoted by 'app.curApk'. - private void installApk(final Apk apk) { + @Override + public void installApk(final Apk apk) { if (isFinishing()) { return; } @@ -1059,51 +268,11 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog InstallManagerService.queue(this, mApp, apk); } - private void cancelInstall() { - if (!TextUtils.isEmpty(mActiveDownloadUrlString)) { - InstallManagerService.cancel(this, mActiveDownloadUrlString); - } - } - - /** - * Queue for uninstall based on the instance variable {@link #app} - */ - private void uninstallApk() { - Apk apk = mApp.installedApk; - if (apk == null) { - // TODO ideally, app would be refreshed immediately after install, then this - // workaround would be unnecessary - try { - PackageInfo pi = getPackageManager().getPackageInfo(mApp.packageName, 0); - apk = ApkProvider.Helper.findApkFromAnyRepo(this, pi.packageName, pi.versionCode); - mApp.installedApk = apk; - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - return; // not installed - } - } - Installer installer = InstallerFactory.create(this, apk); - Intent intent = installer.getUninstallScreen(); - if (intent != null) { - // uninstall screen required - Utils.debugLog(TAG, "screen screen required"); - startActivityForResult(intent, REQUEST_UNINSTALL_DIALOG); - return; - } - - startUninstall(); - } - private void startUninstall() { registerUninstallReceiver(); InstallerService.uninstall(this, mApp.installedApk); } - private void launchApk(String packageName) { - Intent intent = getPackageManager().getLaunchIntentForPackage(packageName); - startActivity(intent); - } - private void registerUninstallReceiver() { mLocalBroadcastManager.registerReceiver(uninstallReceiver, Installer.getUninstallIntentFilter(mApp.packageName)); @@ -1323,14 +492,93 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog return; } AppDetailsRecyclerViewAdapter adapter = (AppDetailsRecyclerViewAdapter)mRecyclerView.getAdapter(); - adapter.updateItems(); + adapter.updateItems(mApp); adapter.notifyDataSetChanged(); supportInvalidateOptionsMenu(); } }); } - private boolean isAppInstalled() { + @Override + public boolean isAppDownloading() { + return !TextUtils.isEmpty(mActiveDownloadUrlString); + } + + @Override + public boolean isAppInstalled() { return mApp.isInstalled(); } + + @Override + public void enableAndroidBeam() { + NfcHelper.setAndroidBeam(this, mApp.packageName); + } + + @Override + public void disableAndroidBeam() { + NfcHelper.disableAndroidBeam(this); + } + + @Override + public void openUrl(String url) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + if (intent.resolveActivity(getPackageManager()) == null) { + Toast.makeText(this, + getString(R.string.no_handler_app, intent.getDataString()), + Toast.LENGTH_LONG).show(); + return; + } + startActivity(intent); + } + + @Override + public void installCancel() { + if (!TextUtils.isEmpty(mActiveDownloadUrlString)) { + InstallManagerService.cancel(this, mActiveDownloadUrlString); + } + } + + @Override + public void launchApk() { + Intent intent = getPackageManager().getLaunchIntentForPackage(mApp.packageName); + startActivity(intent); + } + + @Override + public void installApk() { + Apk apkToInstall = ApkProvider.Helper.findApkFromAnyRepo(AppDetails2.this, mApp.packageName, mApp.suggestedVersionCode); + installApk(apkToInstall); + } + + @Override + public void upgradeApk() { + Apk apkToInstall = ApkProvider.Helper.findApkFromAnyRepo(AppDetails2.this, mApp.packageName, mApp.suggestedVersionCode); + installApk(apkToInstall); + } + + @Override + public void uninstallApk() { + Apk apk = mApp.installedApk; + if (apk == null) { + // TODO ideally, app would be refreshed immediately after install, then this + // workaround would be unnecessary + try { + PackageInfo pi = getPackageManager().getPackageInfo(mApp.packageName, 0); + apk = ApkProvider.Helper.findApkFromAnyRepo(this, pi.packageName, pi.versionCode); + mApp.installedApk = apk; + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return; // not installed + } + } + Installer installer = InstallerFactory.create(this, apk); + Intent intent = installer.getUninstallScreen(); + if (intent != null) { + // uninstall screen required + Utils.debugLog(TAG, "screen screen required"); + startActivityForResult(intent, REQUEST_UNINSTALL_DIALOG); + return; + } + startUninstall(); + } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java new file mode 100644 index 000000000..c964a1422 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java @@ -0,0 +1,830 @@ +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.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; + +import static android.support.v7.widget.RecyclerView.NO_POSITION; + +public class AppDetailsRecyclerViewAdapter + extends RecyclerView.Adapter { + + public interface AppDetailsRecyclerViewAdapterCallbacks { + boolean isAppDownloading(); + boolean isAppInstalled(); + void enableAndroidBeam(); + void disableAndroidBeam(); + void openUrl(String url); + void installApk(); + void installApk(Apk apk); + void upgradeApk(); + void uninstallApk(); + void installCancel(); + void launchApk(); + } + + private final int VIEWTYPE_HEADER = 0; + private final int VIEWTYPE_SCREENSHOTS = 1; + private final int VIEWTYPE_WHATS_NEW = 2; + private final int VIEWTYPE_DONATE = 3; + private final int VIEWTYPE_LINKS = 4; + private final int VIEWTYPE_PERMISSIONS = 5; + private final int VIEWTYPE_VERSIONS = 6; + private final int VIEWTYPE_VERSION = 7; + + private final Context mContext; + private App mApp; + private final AppDetailsRecyclerViewAdapterCallbacks mCallbacks; + private RecyclerView mRecyclerView; + private ArrayList mItems; + private ArrayList mVersions; + private boolean mShowVersions; + + private HeaderViewHolder mHeaderView; + + public AppDetailsRecyclerViewAdapter(Context context, App app, AppDetailsRecyclerViewAdapterCallbacks callbacks) { + mContext = context; + mCallbacks = callbacks; + updateItems(app); + } + + public void updateItems(App app) { + mApp = app; + + // Get versions + mVersions = new ArrayList<>(); + final List apks = ApkProvider.Helper.findByPackageName(mContext, mApp.packageName); + for (final Apk apk : apks) { + if (apk.compatible || Preferences.get().showIncompatibleVersions()) { + mVersions.add(apk); + } + } + + if (mItems == null) + mItems = new ArrayList<>(); + else + mItems.clear(); + addItem(VIEWTYPE_HEADER); + addItem(VIEWTYPE_SCREENSHOTS); + addItem(VIEWTYPE_WHATS_NEW); + addItem(VIEWTYPE_DONATE); + addItem(VIEWTYPE_LINKS); + addItem(VIEWTYPE_PERMISSIONS); + addItem(VIEWTYPE_VERSIONS); + } + + private void setShowVersions(boolean showVersions) { + mShowVersions = showVersions; + mItems.removeAll(mVersions); + if (showVersions) { + mItems.addAll(mItems.indexOf(VIEWTYPE_VERSIONS) + 1, mVersions); + } + 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) { + if (!shouldShowDonate()) + return; + } else if (item == VIEWTYPE_PERMISSIONS) { + if (!shouldShowPermissions()) + return; + } + mItems.add(item); + } + + private boolean shouldShowPermissions() { + // Figure out if we should show permissions section + Apk curApk = null; + for (int i = 0; i < mVersions.size(); i++) { + final Apk apk = mVersions.get(i); + if (apk.versionCode == mApp.suggestedVersionCode) { + curApk = apk; + break; + } + } + final boolean curApkCompatible = curApk != null && curApk.compatible; + return mVersions.size() > 0 && (curApkCompatible || Preferences.get().showIncompatibleVersions()); + } + + private boolean shouldShowDonate() { + return uriIsSetAndCanBeOpened(mApp.donateURL) || + uriIsSetAndCanBeOpened(mApp.bitcoinAddr) || + uriIsSetAndCanBeOpened(mApp.litecoinAddr) || + uriIsSetAndCanBeOpened(mApp.flattrID); + } + + public HeaderViewHolder getHeaderView() { + return mHeaderView; + } + + @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); + } else if (viewType == VIEWTYPE_DONATE) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.app_details2_donate, parent, false); + return new DonateViewHolder(view); + } else if (viewType == VIEWTYPE_LINKS) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.app_details2_links, parent, false); + return new ExpandableLinearLayoutViewHolder(view); + } else if (viewType == VIEWTYPE_PERMISSIONS) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.app_details2_links, parent, false); + return new ExpandableLinearLayoutViewHolder(view); + } else if (viewType == VIEWTYPE_VERSIONS) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.app_details2_links, parent, false); + return new ExpandableLinearLayoutViewHolder(view); + } else if (viewType == VIEWTYPE_VERSION) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.apklistitem, parent, false); + return new VersionViewHolder(view); + } + return null; + } + + @Override + public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) { + int viewType = getItemViewType(position); + if (viewType == VIEWTYPE_HEADER) { + final HeaderViewHolder vh = (HeaderViewHolder) holder; + mHeaderView = vh; + ImageLoader.getInstance().displayImage(mApp.iconUrlLarge, vh.iconView, vh.displayImageOptions); + vh.titleView.setText(mApp.name); + if (!TextUtils.isEmpty(mApp.author)) { + vh.authorView.setText(mContext.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(LinkMovementMethod.getInstance()); + vh.descriptionView.setText(trimNewlines(desc)); + if (vh.descriptionView.getText() instanceof Spannable) { + Spannable spannable = (Spannable) vh.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); + } + } + 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(mCallbacks.isAppInstalled() ? View.VISIBLE : View.INVISIBLE); + vh.buttonSecondaryView.setOnClickListener(mOnUnInstallClickListener); + vh.buttonPrimaryView.setText(R.string.menu_install); + vh.buttonPrimaryView.setVisibility(mVersions.size() > 0 ? View.VISIBLE : View.GONE); + if (mCallbacks.isAppDownloading()) { + vh.buttonPrimaryView.setText(R.string.downloading); + vh.buttonPrimaryView.setEnabled(false); + } else if (!mCallbacks.isAppInstalled() && mApp.suggestedVersionCode > 0 && + mVersions.size() > 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); + mCallbacks.disableAndroidBeam(); + // Set Install button and hide second button + vh.buttonPrimaryView.setText(R.string.menu_install); + vh.buttonPrimaryView.setOnClickListener(mOnInstallClickListener); + vh.buttonPrimaryView.setEnabled(true); + } else if (mCallbacks.isAppInstalled()) { + // If App is installed + //installed = true; + //statusView.setText(getString(R.string.details_installed, app.installedVersionName)); + mCallbacks.enableAndroidBeam(); + if (mApp.canAndWantToUpdate(mContext)) { + //updateWanted = true; + vh.buttonPrimaryView.setText(R.string.menu_upgrade); + vh.buttonPrimaryView.setOnClickListener(mOnUpgradeClickListener); + } else { + //updateWanted = false; + if (mContext.getPackageManager().getLaunchIntentForPackage(mApp.packageName) != null) { + vh.buttonPrimaryView.setText(R.string.menu_launch); + vh.buttonPrimaryView.setOnClickListener(mOnLaunchClickListener); + } else { + vh.buttonPrimaryView.setVisibility(View.GONE); + //vh.buttonPrimaryView.setText(R.string.menu_uninstall); + } + } + vh.buttonPrimaryView.setEnabled(true); + } + if (mCallbacks.isAppDownloading()) { + vh.buttonLayout.setVisibility(View.GONE); + vh.progressLayout.setVisibility(View.VISIBLE); + } else { + vh.buttonLayout.setVisibility(View.VISIBLE); + vh.progressLayout.setVisibility(View.GONE); + } + vh.progressCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mCallbacks.installCancel(); + } + }); + + /*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(vh.itemView.getContext(), mApp); + vh.recyclerView.setAdapter(adapter); + vh.recyclerView.setHasFixedSize(true); + vh.recyclerView.setNestedScrollingEnabled(false); + if (vh.snapHelper != null) + vh.snapHelper.attachToRecyclerView(null); + vh.snapHelper = new LinearLayoutManagerSnapHelper(lm); + vh.snapHelper.setLinearSnapHelperListener(adapter); + vh.snapHelper.attachToRecyclerView(vh.recyclerView); + } else if (viewType == VIEWTYPE_WHATS_NEW) { + WhatsNewViewHolder vh = (WhatsNewViewHolder) holder; + vh.textView.setText("WHATS NEW GOES HERE"); + } else if (viewType == VIEWTYPE_DONATE) { + DonateViewHolder vh = (DonateViewHolder) holder; + vh.contentView.removeAllViews(); + + // Donate button + if (uriIsSetAndCanBeOpened(mApp.donateURL)) { + addLinkItemView(vh.contentView, R.string.menu_donate, R.drawable.ic_donate, mApp.donateURL); + } + + // Bitcoin + if (uriIsSetAndCanBeOpened(mApp.bitcoinAddr)) { + addLinkItemView(vh.contentView, R.string.menu_bitcoin, R.drawable.ic_bitcoin, "bitcoin:" + mApp.bitcoinAddr); + } + + // Litecoin + if (uriIsSetAndCanBeOpened(mApp.litecoinAddr)) { + addLinkItemView(vh.contentView, R.string.menu_litecoin, R.drawable.ic_litecoin, "litecoin:" + mApp.litecoinAddr); + } + + // Flattr + if (uriIsSetAndCanBeOpened(mApp.flattrID)) { + addLinkItemView(vh.contentView, R.string.menu_flattr, R.drawable.ic_flattr, "https://flattr.com/thing/" + mApp.flattrID); + } + } else if (viewType == VIEWTYPE_LINKS) { + final ExpandableLinearLayoutViewHolder vh = (ExpandableLinearLayoutViewHolder) holder; + vh.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + boolean shouldBeVisible = (vh.contentView.getVisibility() != View.VISIBLE); + vh.contentView.setVisibility(shouldBeVisible ? View.VISIBLE : View.GONE); + TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(vh.headerView, R.drawable.ic_website, 0, shouldBeVisible ? R.drawable.ic_expand_less_grey600 : R.drawable.ic_expand_more_grey600, 0); + } + }); + vh.headerView.setText(R.string.links); + TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(vh.headerView, R.drawable.ic_website, 0, R.drawable.ic_expand_more_grey600, 0); + vh.contentView.removeAllViews(); + + // Source button + if (uriIsSetAndCanBeOpened(mApp.sourceURL)) { + addLinkItemView(vh.contentView, R.string.menu_source, R.drawable.ic_source_code, mApp.sourceURL); + } + + // Issues button + if (uriIsSetAndCanBeOpened(mApp.trackerURL)) { + addLinkItemView(vh.contentView, R.string.menu_issues, R.drawable.ic_issues, mApp.trackerURL); + } + + // Changelog button + if (uriIsSetAndCanBeOpened(mApp.changelogURL)) { + addLinkItemView(vh.contentView, R.string.menu_changelog, R.drawable.ic_changelog, mApp.changelogURL); + } + + // Website button + if (uriIsSetAndCanBeOpened(mApp.webURL)) { + addLinkItemView(vh.contentView, R.string.menu_website, R.drawable.ic_website, mApp.webURL); + } + + // Email button + if (uriIsSetAndCanBeOpened(mApp.email)) { + final String subject = Uri.encode(mContext.getString(R.string.app_details_subject, mApp.name)); + String url = "mailto:" + mApp.email + "?subject=" + subject; + addLinkItemView(vh.contentView, R.string.menu_email, R.drawable.ic_email, url); + } + } else if (viewType == VIEWTYPE_PERMISSIONS) { + final ExpandableLinearLayoutViewHolder vh = (ExpandableLinearLayoutViewHolder) holder; + vh.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + boolean shouldBeVisible = (vh.contentView.getVisibility() != View.VISIBLE); + vh.contentView.setVisibility(shouldBeVisible ? View.VISIBLE : View.GONE); + TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(vh.headerView, R.drawable.ic_lock_24dp_grey600, 0, shouldBeVisible ? R.drawable.ic_expand_less_grey600 : R.drawable.ic_expand_more_grey600, 0); + } + }); + vh.headerView.setText(R.string.permissions); + TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(vh.headerView, R.drawable.ic_lock_24dp_grey600, 0, R.drawable.ic_expand_more_grey600, 0); + vh.contentView.removeAllViews(); + AppDiff appDiff = new AppDiff(mContext.getPackageManager(), mVersions.get(0)); + AppSecurityPermissions perms = new AppSecurityPermissions(mContext, appDiff.pkgInfo); + vh.contentView.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL)); + } else if (viewType == VIEWTYPE_VERSIONS) { + final ExpandableLinearLayoutViewHolder vh = (ExpandableLinearLayoutViewHolder) holder; + vh.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setShowVersions(!mShowVersions); + } + }); + vh.headerView.setText(R.string.versions); + TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(vh.headerView, R.drawable.ic_access_time_24dp_grey600, 0, mShowVersions ? R.drawable.ic_expand_less_grey600 : R.drawable.ic_expand_more_grey600, 0); + } else if (viewType == VIEWTYPE_VERSION) { + final VersionViewHolder vh = (VersionViewHolder) holder; + java.text.DateFormat df = DateFormat.getDateFormat(mContext); + final Apk apk = (Apk)mItems.get(position); + + vh.version.setText(mContext.getString(R.string.version) + + " " + apk.versionName + + (apk.versionCode == mApp.suggestedVersionCode ? " ☆" : "")); + + vh.status.setText(getInstalledStatus(apk)); + + vh.repository.setText(mContext.getString(R.string.repo_provider, + RepoProvider.Helper.findById(mContext, apk.repo).getName())); + + if (apk.size > 0) { + vh.size.setText(Utils.getFriendlySize(apk.size)); + vh.size.setVisibility(View.VISIBLE); + } else { + vh.size.setVisibility(View.GONE); + } + + if (!Preferences.get().expertMode()) { + vh.api.setVisibility(View.GONE); + } else if (apk.minSdkVersion > 0 && apk.maxSdkVersion < Apk.SDK_VERSION_MAX_VALUE) { + vh.api.setText(mContext.getString(R.string.minsdk_up_to_maxsdk, + Utils.getAndroidVersionName(apk.minSdkVersion), + Utils.getAndroidVersionName(apk.maxSdkVersion))); + vh.api.setVisibility(View.VISIBLE); + } else if (apk.minSdkVersion > 0) { + vh.api.setText(mContext.getString(R.string.minsdk_or_later, + Utils.getAndroidVersionName(apk.minSdkVersion))); + vh.api.setVisibility(View.VISIBLE); + } else if (apk.maxSdkVersion > 0) { + vh.api.setText(mContext.getString(R.string.up_to_maxsdk, + Utils.getAndroidVersionName(apk.maxSdkVersion))); + vh.api.setVisibility(View.VISIBLE); + } + + if (apk.srcname != null) { + vh.buildtype.setText("source"); + } else { + vh.buildtype.setText("bin"); + } + + if (apk.added != null) { + vh.added.setText(mContext.getString(R.string.added_on, + df.format(apk.added))); + vh.added.setVisibility(View.VISIBLE); + } else { + vh.added.setVisibility(View.GONE); + } + + if (Preferences.get().expertMode() && apk.nativecode != null) { + vh.nativecode.setText(TextUtils.join(" ", apk.nativecode)); + vh.nativecode.setVisibility(View.VISIBLE); + } else { + vh.nativecode.setVisibility(View.GONE); + } + + if (apk.incompatibleReasons != null) { + vh.incompatibleReasons.setText( + mContext.getResources().getString( + R.string.requires_features, + TextUtils.join(", ", apk.incompatibleReasons))); + vh.incompatibleReasons.setVisibility(View.VISIBLE); + } else { + vh.incompatibleReasons.setVisibility(View.GONE); + } + + // Disable it all if it isn't compatible... + final View[] views = { + vh.itemView, + vh.version, + vh.status, + vh.repository, + vh.size, + vh.api, + vh.buildtype, + vh.added, + vh.nativecode, + }; + for (final View v : views) { + v.setEnabled(apk.compatible); + } + vh.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mCallbacks.installApk(apk); + } + }); + } + } + + @Override + public void onViewRecycled(RecyclerView.ViewHolder holder) { + if (holder instanceof HeaderViewHolder) { + mHeaderView = null; + } + super.onViewRecycled(holder); + } + + @Override + public int getItemCount() { + return mItems.size(); + } + + @Override + public int getItemViewType(int position) { + if (mItems.get(position) instanceof Apk) + return VIEWTYPE_VERSION; + return (Integer)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 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) { + // 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 firstChild = mRecyclerView.getChildAt(0); + posOffset = (firstChild == null) ? 0 : (firstChild.getTop()); // - mRecyclerView.getPaddingTop()); + } + 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); + } + + 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); + } + } + } + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + mRecyclerView = recyclerView; + } + + @Override + public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + mRecyclerView = 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); + } + } + + private class WhatsNewViewHolder extends RecyclerView.ViewHolder { + final TextView textView; + + WhatsNewViewHolder(View view) { + super(view); + textView = (TextView) view.findViewById(R.id.text); + } + } + + private class DonateViewHolder extends RecyclerView.ViewHolder { + final TextView textView; + final LinearLayout contentView; + + DonateViewHolder(View view) { + super(view); + textView = (TextView) view.findViewById(R.id.text); + contentView = (LinearLayout) view.findViewById(R.id.ll_information); + } + } + + private 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 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); + } + } + + private void addLinkItemView(ViewGroup parent, int resIdText, int resIdDrawable, String url) { + TextView view = (TextView) LayoutInflater.from(parent.getContext()).inflate(R.layout.app_details2_link_item, parent, false); + view.setTag(url); + 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((String) v.getTag()); + } + }); + } + + private String getInstalledStatus(final Apk apk) { + // Definitely not installed. + if (apk.versionCode != mApp.installedVersionCode) { + return mContext.getString(R.string.app_not_installed); + } + // Definitely installed this version. + if (apk.sig != null && apk.sig.equals(mApp.installedSig)) { + return mContext.getString(R.string.app_installed); + } + // Installed the same version, but from someplace else. + final String installerPkgName; + try { + installerPkgName = mContext.getPackageManager().getInstallerPackageName(mApp.packageName); + } catch (IllegalArgumentException e) { + Log.w("AppDetailsAdapter", "Application " + mApp.packageName + " is not installed anymore"); + return mContext.getString(R.string.app_not_installed); + } + if (TextUtils.isEmpty(installerPkgName)) { + return mContext.getString(R.string.app_inst_unknown_source); + } + final String installerLabel = InstalledAppProvider + .getApplicationLabel(mContext, installerPkgName); + return mContext.getString(R.string.app_inst_known_source, installerLabel); + } + + private void onLinkClicked(String url) { + if (!TextUtils.isEmpty(url)) { + mCallbacks.openUrl(url); + } + } + + private View.OnClickListener mOnInstallClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + mCallbacks.installApk(); + } + }; + + private View.OnClickListener mOnUnInstallClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + mCallbacks.uninstallApk(); + } + }; + + private View.OnClickListener mOnUpgradeClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + mCallbacks.upgradeApk(); + } + }; + + private View.OnClickListener mOnLaunchClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + mCallbacks.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(mContext.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). + 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); + } + + @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(); + } + } + } +} diff --git a/app/src/main/res/layout/app_details2_screenshot_item.xml b/app/src/main/res/layout/app_details2_screenshot_item.xml index 02f2b6057..114dc2269 100644 --- a/app/src/main/res/layout/app_details2_screenshot_item.xml +++ b/app/src/main/res/layout/app_details2_screenshot_item.xml @@ -6,14 +6,13 @@ app:cardElevation="3dp" app:cardBackgroundColor="#ffffff" android:padding="10dp" - android:layout_marginTop="16dp" - android:layout_marginBottom="16dp" > \ No newline at end of file diff --git a/app/src/main/res/layout/app_details2_screenshots.xml b/app/src/main/res/layout/app_details2_screenshots.xml index 94eb2f708..4d8d98c7a 100644 --- a/app/src/main/res/layout/app_details2_screenshots.xml +++ b/app/src/main/res/layout/app_details2_screenshots.xml @@ -3,5 +3,5 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/screenshots" android:layout_width="match_parent" - android:layout_height="150dp"> + android:layout_height="@dimen/details_screenshot_height"> \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 0cd780d2b..c85219705 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -5,6 +5,9 @@ 48dp 180dp 8dp + + 200dp + 200dp 8dp