diff --git a/res/layout-land/app_details.xml b/res/layout-land/app_details.xml new file mode 100644 index 000000000..d7ff9d84f --- /dev/null +++ b/res/layout-land/app_details.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/res/layout-land/appdetails.xml b/res/layout-land/appdetails.xml deleted file mode 100644 index 9f91b96ff..000000000 --- a/res/layout-land/appdetails.xml +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/res/layout/app_details.xml b/res/layout/app_details.xml new file mode 100644 index 000000000..3acfc0973 --- /dev/null +++ b/res/layout/app_details.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/res/layout/app_details_summary.xml b/res/layout/app_details_summary.xml new file mode 100644 index 000000000..44c9afe11 --- /dev/null +++ b/res/layout/app_details_summary.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/appdetails.xml b/res/layout/appdetails.xml deleted file mode 100644 index 686eb91d5..000000000 --- a/res/layout/appdetails.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/res/values/ids.xml b/res/values/ids.xml index 23d937f3d..2d82ad59c 100644 --- a/res/values/ids.xml +++ b/res/values/ids.xml @@ -1,4 +1,5 @@ + \ No newline at end of file diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index fca4c0b10..60a3851b6 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -21,10 +21,12 @@ package org.fdroid.fdroid; import android.app.Activity; import android.app.AlertDialog; -import android.app.ListActivity; import android.app.ProgressDialog; import android.bluetooth.BluetoothAdapter; -import android.content.*; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; @@ -34,12 +36,12 @@ import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.Handler; -import android.preference.PreferenceManager; +import android.support.v4.app.Fragment; +import android.support.v4.app.ListFragment; import android.support.v4.app.NavUtils; import android.support.v4.view.MenuItemCompat; -import android.text.Editable; +import android.support.v7.app.ActionBarActivity; import android.text.Html; -import android.text.Html.TagHandler; import android.text.Spanned; import android.text.format.DateFormat; import android.text.method.LinkMovementMethod; @@ -52,8 +54,8 @@ import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.widget.ArrayAdapter; +import android.widget.FrameLayout; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; @@ -64,20 +66,45 @@ import org.fdroid.fdroid.Utils.CommaSeparatedList; import org.fdroid.fdroid.compat.ActionBarCompat; import org.fdroid.fdroid.compat.MenuManager; import org.fdroid.fdroid.compat.PackageManagerCompat; -import org.fdroid.fdroid.data.*; +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.Repo; +import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.installer.Installer.AndroidNotCompatibleException; import org.fdroid.fdroid.installer.Installer.InstallerCallback; import org.fdroid.fdroid.net.ApkDownloader; import org.fdroid.fdroid.net.Downloader; -import org.xml.sax.XMLReader; import java.io.File; import java.security.NoSuchAlgorithmException; import java.util.Iterator; import java.util.List; -public class AppDetails extends ListActivity implements ProgressListener { +interface AppDetailsData { + public App getApp(); + public AppDetails.ApkListAdapter getApks(); + public Signature getInstalledSignature(); + public String getInstalledSignatureId(); +} + +/** + * Interface which allows the apk list fragment to communicate with the activity when + * a user requests to install/remove an apk by clicking on an item in the list. + * + * NOTE: This is not to do with with the sudo/packagemanager/other installer + * stuff which allows multiple ways to install apps. It is only here to make fragment- + * activity communication possible. + */ +interface AppInstallListener { + public void install(final Apk apk); + public void removeApk(String packageName); +} + +public class AppDetails extends ActionBarActivity implements ProgressListener, AppDetailsData, AppInstallListener { + private static final String TAG = "org.fdroid.fdroid.AppDetails"; public static final int REQUEST_ENABLE_BLUETOOTH = 2; @@ -118,14 +145,13 @@ public class AppDetails extends ListActivity implements ProgressListener { AppDetails.this.finish(); return; } - updateViews(); + refreshApkList(); MenuManager.create(AppDetails.this).invalidateOptionsMenu(); - } + } } - - private class ApkListAdapter extends ArrayAdapter { + class ApkListAdapter extends ArrayAdapter { private LayoutInflater mInflater = (LayoutInflater) mctx.getSystemService( Context.LAYOUT_INFLATER_SERVICE); @@ -134,7 +160,7 @@ public class AppDetails extends ListActivity implements ProgressListener { super(context, 0); List apks = ApkProvider.Helper.findByApp(context, app.id); for (Apk apk : apks ) { - if (apk.compatible || pref_incompatibleVersions) { + if (apk.compatible || Preferences.get().showIncompatibleVersions()) { add(apk); } } @@ -149,7 +175,7 @@ public class AppDetails extends ListActivity implements ProgressListener { ViewHolder holder; if (convertView == null) { - convertView = mInflater.inflate(R.layout.apklistitem, null); + convertView = mInflater.inflate(R.layout.apklistitem, parent, false); holder = new ViewHolder(); holder.version = (TextView) convertView.findViewById(R.id.version); @@ -185,7 +211,7 @@ public class AppDetails extends ListActivity implements ProgressListener { holder.size.setVisibility(View.GONE); } - if (!pref_expert) { + if (!Preferences.get().expertMode()) { holder.api.setVisibility(View.GONE); } else if (apk.minSdkVersion > 0 && apk.maxSdkVersion > 0) { holder.api.setText(getString(R.string.minsdk_up_to_maxsdk, @@ -216,7 +242,7 @@ public class AppDetails extends ListActivity implements ProgressListener { holder.added.setVisibility(View.GONE); } - if (pref_expert && apk.nativecode != null) { + if (Preferences.get().expertMode() && apk.nativecode != null) { holder.nativecode.setText(apk.nativecode.toString().replaceAll(","," ")); holder.nativecode.setVisibility(View.VISIBLE); } else { @@ -277,11 +303,7 @@ public class AppDetails extends ListActivity implements ProgressListener { private boolean startingIgnoreAll; private int startingIgnoreThis; - LinearLayout headerView; - View infoView; - private final Context mctx = this; - private DisplayImageOptions displayImageOptions; private Installer installer; /** @@ -337,9 +359,9 @@ public class AppDetails extends ListActivity implements ProgressListener { // fdroid.app:app.id appId = data.getEncodedSchemeSpecificPart(); } - Log.d("FDroid", "AppDetails launched from link, for '" + appId + "'"); + Log.d(TAG, "AppDetails launched from link, for '" + appId + "'"); } else if (!i.hasExtra(EXTRA_APPID)) { - Log.e("FDroid", "No application ID in AppDetails!?"); + Log.e(TAG, "No application ID in AppDetails!?"); } else { appId = i.getStringExtra(EXTRA_APPID); } @@ -349,39 +371,22 @@ public class AppDetails extends ListActivity implements ProgressListener { @Override protected void onCreate(Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); - + fdroidApp = ((FDroidApp) getApplication()); fdroidApp.applyTheme(this); super.onCreate(savedInstanceState); - 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(); - - setContentView(R.layout.appdetails); - - // Actionbar cannot be accessed until after setContentView (on 3.0 and 3.1 devices) - // see: http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html - // for reason why. - ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); - if (getIntent().hasExtra(EXTRA_FROM)) { setTitle(getIntent().getStringExtra(EXTRA_FROM)); } mPm = getPackageManager(); - installer = Installer.getActivityInstaller(this, mPm, - myInstallerCallback); + installer = Installer.getActivityInstaller(this, mPm, myInstallerCallback); // Get the preferences we're going to use in this Activity... - ConfigurationChangeHelper previousData = (ConfigurationChangeHelper)getLastNonConfigurationInstance(); + ConfigurationChangeHelper previousData = (ConfigurationChangeHelper)getLastCustomNonConfigurationInstance(); if (previousData != null) { Log.d(TAG, "Recreating view after configuration change."); downloadHandler = previousData.downloader; @@ -397,28 +402,34 @@ public class AppDetails extends ListActivity implements ProgressListener { } } - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(getBaseContext()); - pref_expert = prefs.getBoolean(Preferences.PREF_EXPERT, false); - pref_permissions = prefs.getBoolean(Preferences.PREF_PERMISSIONS, false); - pref_incompatibleVersions = prefs.getBoolean( - Preferences.PREF_INCOMP_VER, false); - // Set up the list... - headerView = new LinearLayout(this); - ListView lv = (ListView) findViewById(android.R.id.list); - lv.addHeaderView(headerView); adapter = new ApkListAdapter(this, app); - setListAdapter(adapter); - startViews(); + // Wait until all other intialization before doing this, because it will create the + // fragments, which rely on data from the activity that is set earlier in this method. + setContentView(R.layout.app_details); + + // Actionbar cannot be accessed until after setContentView (on 3.0 and 3.1 devices) + // see: http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html + // for reason why. + ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); + + // Check for the presence of a view which only exists in the landscape view. + // This seems to be the preferred way to interrogate the view, rather than + // to check the orientation. I guess this is because views can be dynamically + // chosen based on more than just orientation (e.g. large screen sizes). + View onlyInLandscape = findViewById(R.id.app_summary_container); + + AppDetailsListFragment listFragment = + (AppDetailsListFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_app_list); + if (onlyInLandscape == null) { + listFragment.setupSummaryHeader(); + } else { + listFragment.removeSummaryHeader(); + } } - private boolean pref_expert; - private boolean pref_permissions; - private boolean pref_incompatibleVersions; - // The signature of the installed version. private Signature mInstalledSignature; private String mInstalledSigID; @@ -426,13 +437,13 @@ public class AppDetails extends ListActivity implements ProgressListener { @Override protected void onResume() { super.onResume(); - + // register observer to know when install status changes myAppObserver = new AppObserver(new Handler()); getContentResolver().registerContentObserver( - AppProvider.getContentUri(app.id), - true, - myAppObserver); + AppProvider.getContentUri(app.id), + true, + myAppObserver); if (downloadHandler != null) { if (downloadHandler.isComplete()) { downloadCompleteInstallApk(); @@ -446,9 +457,12 @@ public class AppDetails extends ListActivity implements ProgressListener { updateProgressDialog(); } } + } - updateViews(); - + @Override + protected void onResumeFragments() { + super.onResumeFragments(); + refreshApkList(); MenuManager.create(this).invalidateOptionsMenu(); } @@ -509,7 +523,7 @@ public class AppDetails extends ListActivity implements ProgressListener { @Override - public Object onRetainNonConfigurationInstance() { + public Object onRetainCustomNonConfigurationInstance() { inProcessOfChangingConfiguration = true; return new ConfigurationChangeHelper(downloadHandler, app); } @@ -538,7 +552,7 @@ public class AppDetails extends ListActivity implements ProgressListener { // Return true if the app was found, false otherwise. private boolean reset(String appId) { - Log.d("FDroid", "Getting application details for " + appId); + Log.d(TAG, "Getting application details for " + appId); App newApp = null; if (appId != null && appId.length() > 0) { @@ -579,233 +593,22 @@ public class AppDetails extends ListActivity implements ProgressListener { Hasher hash = new Hasher("MD5", mInstalledSignature.toCharsString().getBytes()); mInstalledSigID = hash.getHash(); } catch (NameNotFoundException e) { - Log.d("FDroid", "Failed to get installed signature"); + Log.d(TAG, "Failed to get installed signature"); } catch (NoSuchAlgorithmException e) { - Log.d("FDroid", "Failed to calculate signature MD5 sum"); + Log.d(TAG, "Failed to calculate signature MD5 sum"); mInstalledSignature = null; } } } - private void startViews() { - - // Insert the 'infoView' (which contains the summary, various odds and - // ends, and the description) into the appropriate place, if we're in - // landscape mode. In portrait mode, we put it in the listview's - // header.. - infoView = View.inflate(this, R.layout.appinfo, null); - LinearLayout landparent = (LinearLayout) findViewById(R.id.landleft); - headerView.removeAllViews(); - if (landparent != null) { - landparent.addView(infoView); - Log.d("FDroid", "Setting up landscape view"); - } else { - headerView.addView(infoView); - Log.d("FDroid", "Setting up portrait view"); - } - - // Set the icon... - ImageView iv = (ImageView) findViewById(R.id.icon); - ImageLoader.getInstance().displayImage(app.iconUrl, iv, - displayImageOptions); - - // Set the title and other header details... - TextView tv = (TextView) findViewById(R.id.title); - tv.setText(app.name); - tv = (TextView) findViewById(R.id.license); - tv.setText(app.license); - - if (app.categories != null) { - tv = (TextView) findViewById(R.id.categories); - tv.setText(app.categories.toString().replaceAll(",",", ")); - } - - tv = (TextView) infoView.findViewById(R.id.description); - - tv.setMovementMethod(LinkMovementMethod.getInstance()); - - // Need this to add the unimplemented support for ordered and unordered - // lists to Html.fromHtml(). - class HtmlTagHandler implements TagHandler { - int listNum; - - @Override - public void handleTag(boolean opening, String tag, Editable output, - XMLReader reader) { - if (tag.equals("ul")) { - if (opening) - listNum = -1; - else - output.append('\n'); - } else if (opening && tag.equals("ol")) { - if (opening) - listNum = 1; - else - output.append('\n'); - } else if (tag.equals("li")) { - if (opening) { - if (listNum == -1) { - output.append("\t• "); - } else { - output.append("\t").append(Integer.toString(listNum)).append(". "); - listNum++; - } - } else { - output.append('\n'); - } - } - } - } - Spanned desc = Html.fromHtml( - app.description, null, new HtmlTagHandler()); - tv.setText(desc.subSequence(0, desc.length() - 2)); - - tv = (TextView) infoView.findViewById(R.id.appid); - if (pref_expert) - tv.setText(app.id); - else - tv.setVisibility(View.GONE); - - tv = (TextView) infoView.findViewById(R.id.summary); - tv.setText(app.summary); - - Apk curApk = null; - for (int i = 0; i < adapter.getCount(); i ++) { - Apk apk = adapter.getItem(i); - if (apk.vercode == app.suggestedVercode) { - curApk = apk; - break; - } - } - - if (pref_permissions && !adapter.isEmpty() && - ((curApk != null && curApk.compatible) || pref_incompatibleVersions)) { - tv = (TextView) infoView.findViewById(R.id.permissions_list); - - CommaSeparatedList permsList = adapter.getItem(0).permissions; - if (permsList == null) { - tv.setText(getString(R.string.no_permissions)); - } else { - Iterator permissions = permsList.iterator(); - StringBuilder sb = new StringBuilder(); - while (permissions.hasNext()) { - String permissionName = permissions.next(); - try { - Permission permission = new Permission(this, permissionName); - sb.append("\t• ").append(permission.getName()).append('\n'); - } catch (NameNotFoundException e) { - if (permissionName.equals("ACCESS_SUPERUSER")) { - sb.append("\t• Full permissions to all device features and storage\n"); - } else { - Log.d("FDroid", "Permission not yet available: " + permissionName); - } - } - } - if (sb.length() > 0) sb.setLength(sb.length() - 1); - tv.setText(sb.toString()); - } - tv = (TextView) infoView.findViewById(R.id.permissions); - tv.setText(getString( - R.string.permissions_for_long, adapter.getItem(0).version)); - } else { - infoView.findViewById(R.id.permissions).setVisibility(View.GONE); - infoView.findViewById(R.id.permissions_list).setVisibility(View.GONE); - } - - tv = (TextView) infoView.findViewById(R.id.antifeatures); - if (app.antiFeatures != null) { - StringBuilder sb = new StringBuilder(); - for (String af : app.antiFeatures) { - String afdesc = descAntiFeature(af); - if (afdesc != null) { - sb.append("\t• ").append(afdesc).append("\n"); - } - } - if (sb.length() > 0) { - sb.setLength(sb.length() - 1); - tv.setText(sb.toString()); - } else { - tv.setVisibility(View.GONE); - } - } else { - tv.setVisibility(View.GONE); - } - } - - private String descAntiFeature(String af) { - if (af.equals("Ads")) - return getString(R.string.antiadslist); - if (af.equals("Tracking")) - return getString(R.string.antitracklist); - if (af.equals("NonFreeNet")) - return getString(R.string.antinonfreenetlist); - if (af.equals("NonFreeAdd")) - return getString(R.string.antinonfreeadlist); - if (af.equals("NonFreeDep")) - return getString(R.string.antinonfreedeplist); - if (af.equals("UpstreamNonFree")) - return getString(R.string.antiupstreamnonfreelist); - return null; - } - - private void updateViews() { - - // Refresh the list... + private void refreshApkList() { adapter.notifyDataSetChanged(); - - TextView tv = (TextView) findViewById(R.id.status); - if (app.isInstalled()) { - tv.setText(getString(R.string.details_installed, - app.installedVersionName)); - NfcBeamManager.setAndroidBeam(this, app.id); - } else { - tv.setText(getString(R.string.details_notinstalled)); - NfcBeamManager.disableAndroidBeam(this); - } - - tv = (TextView) infoView.findViewById(R.id.signature); - if (pref_expert && mInstalledSignature != null) { - tv.setVisibility(View.VISIBLE); - tv.setText("Signed: " + mInstalledSigID); - } else { - tv.setVisibility(View.GONE); - } - - } - - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - final Apk apk = adapter.getItem(position - l.getHeaderViewsCount()); - if (app.installedVersionCode == apk.vercode) - removeApk(app.id); - else if (app.installedVersionCode > apk.vercode) { - AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this); - ask_alrt.setMessage(getString(R.string.installDowngrade)); - ask_alrt.setPositiveButton(getString(R.string.yes), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int whichButton) { - install(apk); - } - }); - ask_alrt.setNegativeButton(getString(R.string.no), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int whichButton) { - } - }); - AlertDialog alert = ask_alrt.create(); - alert.show(); - } else - install(apk); } @Override public boolean onPrepareOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); + super.onPrepareOptionsMenu(menu); menu.clear(); if (app == null) return true; @@ -997,8 +800,7 @@ public class AppDetails extends ListActivity implements ProgressListener { } // Install the version of this app denoted by 'app.curApk'. - private void install(final Apk apk) { - final Activity activity = this; + public void install(final Apk apk) { String [] projection = { RepoProvider.DataColumns.ADDRESS }; Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection); if (repo == null || repo.address == null) { @@ -1064,7 +866,8 @@ public class AppDetails extends ListActivity implements ProgressListener { } } - private void removeApk(String packageName) { + @Override + public void removeApk(String packageName) { setProgressBarIndeterminateVisibility(true); try { @@ -1253,4 +1056,327 @@ public class AppDetails extends ListActivity implements ProgressListener { break; } } -} + + public App getApp() { + return app; + } + + public ApkListAdapter getApks() { + return adapter; + } + + public Signature getInstalledSignature() { + return mInstalledSignature; + } + + public String getInstalledSignatureId() { + return mInstalledSigID; + } + + public static class AppDetailsSummaryFragment extends Fragment { + + protected final Preferences prefs; + protected final DisplayImageOptions displayImageOptions; + private AppDetailsData data; + + public AppDetailsSummaryFragment() { + prefs = Preferences.get(); + displayImageOptions = new DisplayImageOptions.Builder() + .cacheInMemory(true) + .cacheOnDisk(true) + .imageScaleType(ImageScaleType.NONE) + .showImageOnLoading(R.drawable.ic_repo_app_default) + .showImageForEmptyUri(R.drawable.ic_repo_app_default) + .bitmapConfig(Bitmap.Config.RGB_565) + .build(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + data = (AppDetailsData)activity; + } + + protected App getApp() { + return data.getApp(); + } + + protected ApkListAdapter getApks() { + return data.getApks(); + } + + protected Signature getInstalledSignature() { + return data.getInstalledSignature(); + } + + protected String getInstalledSignatureId() { + return data.getInstalledSignatureId(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View summaryView = inflater.inflate(R.layout.app_details_summary, container, false); + setupView(summaryView); + return summaryView; + } + + @Override + public void onResume() { + super.onResume(); + updateViews(getView()); + } + + private void setupView(View view) { + + // Set the icon... + ImageView iv = (ImageView) view.findViewById(R.id.icon); + ImageLoader.getInstance().displayImage(getApp().iconUrl, iv, displayImageOptions); + + // Set the title and other header details... + TextView tv = (TextView) view.findViewById(R.id.title); + tv.setText(getApp().name); + tv = (TextView) view.findViewById(R.id.license); + tv.setText(getApp().license); + + if (getApp().categories != null) { + tv = (TextView) view.findViewById(R.id.categories); + tv.setText(getApp().categories.toString().replaceAll(",", ", ")); + } + + TextView description = (TextView) view.findViewById(R.id.description); + Spanned desc = Html.fromHtml(getApp().description, null, new Utils.HtmlTagHandler()); + description.setMovementMethod(LinkMovementMethod.getInstance()); + description.setText(desc.subSequence(0, desc.length() - 2)); + + TextView appIdView = (TextView) view.findViewById(R.id.appid); + if (prefs.expertMode()) + appIdView.setText(getApp().id); + else + appIdView.setVisibility(View.GONE); + + TextView summaryView = (TextView) view.findViewById(R.id.summary); + summaryView.setText(getApp().summary); + + Apk curApk = null; + for (int i = 0; i < getApks().getCount(); i ++) { + Apk apk = getApks().getItem(i); + if (apk.vercode == getApp().suggestedVercode) { + curApk = apk; + break; + } + } + + TextView permissionListView = (TextView) view.findViewById(R.id.permissions_list); + TextView permissionHeader = (TextView) view.findViewById(R.id.permissions); + boolean curApkCompatible = curApk != null && curApk.compatible; + if (prefs.showPermissions() && !getApks().isEmpty() && + ( curApkCompatible || prefs.showIncompatibleVersions() ) ) { + + CommaSeparatedList permsList = getApks().getItem(0).permissions; + if (permsList == null) { + permissionListView.setText(getString(R.string.no_permissions)); + } else { + Iterator permissions = permsList.iterator(); + StringBuilder sb = new StringBuilder(); + while (permissions.hasNext()) { + String permissionName = permissions.next(); + try { + Permission permission = new Permission(getActivity(), permissionName); + // TODO: Make this list RTL friendly + sb.append("\t• ").append(permission.getName()).append('\n'); + } catch (NameNotFoundException e) { + if (permissionName.equals("ACCESS_SUPERUSER")) { + // TODO: i18n this string, but surely it is already translated somewhere? + sb.append("\t• Full permissions to all device features and storage\n"); + } else { + Log.e(TAG, "Permission not yet available: " + permissionName); + } + } + } + if (sb.length() > 0) sb.setLength(sb.length() - 1); + permissionListView.setText(sb.toString()); + } + permissionHeader.setText(getString(R.string.permissions_for_long, getApks().getItem(0).version)); + } else { + permissionListView.setVisibility(View.GONE); + permissionHeader.setVisibility(View.GONE); + } + + TextView antiFeaturesView = (TextView) view.findViewById(R.id.antifeatures); + if (getApp().antiFeatures != null) { + StringBuilder sb = new StringBuilder(); + for (String af : getApp().antiFeatures) { + String afdesc = descAntiFeature(af); + if (afdesc != null) { + sb.append("\t• ").append(afdesc).append("\n"); + } + } + if (sb.length() > 0) { + sb.setLength(sb.length() - 1); + antiFeaturesView.setText(sb.toString()); + } else { + antiFeaturesView.setVisibility(View.GONE); + } + } else { + antiFeaturesView.setVisibility(View.GONE); + } + + updateViews(view); + } + + private String descAntiFeature(String af) { + if (af.equals("Ads")) + return getString(R.string.antiadslist); + if (af.equals("Tracking")) + return getString(R.string.antitracklist); + if (af.equals("NonFreeNet")) + return getString(R.string.antinonfreenetlist); + if (af.equals("NonFreeAdd")) + return getString(R.string.antinonfreeadlist); + if (af.equals("NonFreeDep")) + return getString(R.string.antinonfreedeplist); + if (af.equals("UpstreamNonFree")) + return getString(R.string.antiupstreamnonfreelist); + return null; + } + + public void updateViews(View view) { + + if (view == null) { + Log.e(TAG, "AppDetailsSummaryFragment.refreshApkList - view == null. Oops."); + return; + } + + TextView statusView = (TextView) view.findViewById(R.id.status); + if (getApp().isInstalled()) { + statusView.setText(getString(R.string.details_installed, getApp().installedVersionName)); + NfcBeamManager.setAndroidBeam(getActivity(), getApp().id); + } else { + statusView.setText(getString(R.string.details_notinstalled)); + NfcBeamManager.disableAndroidBeam(getActivity()); + } + + TextView signatureView = (TextView) view.findViewById(R.id.signature); + if (prefs.expertMode() && getInstalledSignature() != null) { + signatureView.setVisibility(View.VISIBLE); + signatureView.setText("Signed: " + getInstalledSignatureId()); + } else { + signatureView.setVisibility(View.GONE); + } + + } + } + + public static class AppDetailsListFragment extends ListFragment { + + private final String SUMMARY_TAG = "summary"; + + private AppDetailsData data; + private AppInstallListener installListener; + private AppDetailsSummaryFragment summaryFragment = null; + + private FrameLayout headerView; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + data = (AppDetailsData)activity; + installListener = (AppInstallListener)activity; + } + + protected void install(final Apk apk) { + installListener.install(apk); + } + + protected void remove() { + installListener.removeApk(getApp().id); + } + + protected App getApp() { + return data.getApp(); + } + + protected ApkListAdapter getApks() { + return data.getApks(); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + // A bit of a hack, but we can't add the header view in setupSummaryHeader(), + // due to the fact it needs to happen before setListAdapter(). Also, seeing + // as we may never add a summary header (i.e. in landscape), this is probably + // the last opportunity to set the list adapter. As such, we use the headerView + // as a mechanism to optionally allow adding a header in the future. + if (headerView == null) { + headerView = new FrameLayout(getActivity().getApplicationContext()); + headerView.setId(R.id.appDetailsSummaryHeader); + } else { + Fragment summaryFragment = getChildFragmentManager().findFragmentByTag(SUMMARY_TAG); + if (summaryFragment != null) { + getChildFragmentManager().beginTransaction().remove(summaryFragment).commit(); + } + } + + setListAdapter(null); + getListView().addHeaderView(headerView); + setListAdapter(getApks()); + } + + @Override + public void onResume() { + super.onResume(); + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + final Apk apk = getApks().getItem(position - l.getHeaderViewsCount()); + if (getApp().installedVersionCode == apk.vercode) + remove(); + else if (getApp().installedVersionCode > apk.vercode) { + AlertDialog.Builder ask_alrt = new AlertDialog.Builder(getActivity()); + ask_alrt.setMessage(getString(R.string.installDowngrade)); + ask_alrt.setPositiveButton(getString(R.string.yes), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int whichButton) { + install(apk); + } + }); + ask_alrt.setNegativeButton(getString(R.string.no), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int whichButton) { + } + }); + AlertDialog alert = ask_alrt.create(); + alert.show(); + } else + install(apk); + } + + public void removeSummaryHeader() { + Fragment summary = getChildFragmentManager().findFragmentByTag(SUMMARY_TAG); + if (summary != null) { + getChildFragmentManager().beginTransaction().remove(summary).commit(); + headerView.removeAllViews(); + headerView.setVisibility(View.GONE); + summaryFragment = null; + } + } + + public void setupSummaryHeader() { + Fragment fragment = getChildFragmentManager().findFragmentByTag(SUMMARY_TAG); + if (fragment != null) { + summaryFragment = (AppDetailsSummaryFragment)fragment; + } else { + summaryFragment = new AppDetailsSummaryFragment(); + } + getChildFragmentManager().beginTransaction().replace(headerView.getId(), summaryFragment, SUMMARY_TAG).commit(); + headerView.setVisibility(View.VISIBLE); + } + } + +} \ No newline at end of file diff --git a/src/org/fdroid/fdroid/Preferences.java b/src/org/fdroid/fdroid/Preferences.java index ca6d750fd..4254617a7 100644 --- a/src/org/fdroid/fdroid/Preferences.java +++ b/src/org/fdroid/fdroid/Preferences.java @@ -1,13 +1,19 @@ package org.fdroid.fdroid; -import java.util.*; - import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; import android.util.Log; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + /** * Handles shared preferences for FDroid, looking after the names of * preferences, default values and caching. Needs to be setup in the FDroidApp @@ -55,6 +61,9 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi private static final boolean DEFAULT_SYSTEM_INSTALLER = false; private static final boolean DEFAULT_LOCAL_REPO_BONJOUR = true; private static final boolean DEFAULT_LOCAL_REPO_HTTPS = false; + private static final boolean DEFAULT_INCOMP_VER = false; + private static final boolean DEFAULT_EXPERT = false; + private static final boolean DEFAULT_PERMISSIONS = false; private boolean compactLayout = DEFAULT_COMPACT_LAYOUT; private boolean filterAppsRequiringRoot = DEFAULT_ROOTED; @@ -92,6 +101,18 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi return preferences.getBoolean(PREF_LOCAL_REPO_BONJOUR, DEFAULT_LOCAL_REPO_BONJOUR); } + public boolean showIncompatibleVersions() { + return preferences.getBoolean(PREF_INCOMP_VER, DEFAULT_INCOMP_VER); + } + + public boolean showPermissions() { + return preferences.getBoolean(PREF_PERMISSIONS, DEFAULT_PERMISSIONS); + } + + public boolean expertMode() { + return preferences.getBoolean(PREF_EXPERT, DEFAULT_EXPERT); + } + public boolean isLocalRepoHttpsEnabled() { return preferences.getBoolean(PREF_LOCAL_REPO_HTTPS, DEFAULT_LOCAL_REPO_HTTPS); } diff --git a/src/org/fdroid/fdroid/Utils.java b/src/org/fdroid/fdroid/Utils.java index cd1322f10..ea97f786d 100644 --- a/src/org/fdroid/fdroid/Utils.java +++ b/src/org/fdroid/fdroid/Utils.java @@ -23,24 +23,41 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.AssetManager; import android.content.res.XmlResourceParser; import android.net.Uri; +import android.text.Editable; +import android.text.Html; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; - +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListAdapter; +import android.widget.ListView; import com.nostra13.universalimageloader.utils.StorageUtils; - import org.fdroid.fdroid.data.Repo; +import org.xml.sax.XMLReader; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import java.io.*; +import java.io.BufferedInputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.text.SimpleDateFormat; -import java.util.*; +import java.util.Formatter; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; public final class Utils { @@ -452,4 +469,38 @@ public final class Utils { return String.format("%0" + (bytes.length << 1) + "X", bi); } + + // Need this to add the unimplemented support for ordered and unordered + // lists to Html.fromHtml(). + public static class HtmlTagHandler implements Html.TagHandler { + int listNum; + + @Override + public void handleTag(boolean opening, String tag, Editable output, + XMLReader reader) { + if (tag.equals("ul")) { + if (opening) + listNum = -1; + else + output.append('\n'); + } else if (opening && tag.equals("ol")) { + if (opening) + listNum = 1; + else + output.append('\n'); + } else if (tag.equals("li")) { + if (opening) { + if (listNum == -1) { + output.append("\t• "); + } else { + output.append("\t").append(Integer.toString(listNum)).append(". "); + listNum++; + } + } else { + output.append('\n'); + } + } + } + } + }