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');
+ }
+ }
+ }
+ }
+
}