Rotation of app details with fragments implemented.

This was a bit more complex than all the other views, because it supports
rotation, and different views for when it is rotated. The end result is
that the way in which the views were constructed needed to be completely
redone.

In the process, I also moved the layout of the app summary to a Relative
Layout. This adds more flexibility, and is also the suggested layout
for complex views (as apposed to nested linear layouts). I believe this
is due to the performance of relative vs linear layotus.

It was aprticularly hard to figure out what was going on
when rotating an Activity which had a list fragment
that had another fragment as a header. I don't think fragments
were designed to work like this, but I believe it is all working
as expected now.

Conflicts:
	src/org/fdroid/fdroid/Preferences.java
This commit is contained in:
Peter Serwylo 2014-06-03 08:26:14 +09:30 committed by Hans-Christoph Steiner
parent 659b46fd4e
commit b82be525b9
9 changed files with 695 additions and 487 deletions

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:padding="5dp"
android:baselineAligned="false"
android:orientation="horizontal">
<ScrollView
android:id="@+id/app_summary_container"
android:layout_width="0px"
android:layout_weight="0.5"
android:layout_height="wrap_content">
<fragment
android:id="@+id/fragment_app_summary"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:name="org.fdroid.fdroid.AppDetails$AppDetailsSummaryFragment"
tools:layout="@layout/app_details_summary"/>
</ScrollView>
<fragment
android:id="@+id/fragment_app_list"
android:layout_width="0px"
android:layout_weight="0.5"
android:layout_height="wrap_content"
android:name="org.fdroid.fdroid.AppDetails$AppDetailsListFragment"
tools:layout="@android:layout/list_content"/>
</LinearLayout>

View File

@ -1,91 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:baselineAligned="false"
android:orientation="horizontal" >
<ScrollView
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_weight="0.5" >
<LinearLayout
android:id="@+id/landleft"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
android:orientation="vertical" >
<TextView
android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="false"
android:textSize="18sp"
android:textStyle="bold" />
<RelativeLayout
android:id="@+id/header"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:orientation="horizontal" >
<ImageView
android:id="@+id/icon"
android:contentDescription="@string/app_icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:padding="4dp"
android:scaleType="fitCenter"
/>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="4dp"
android:layout_toRightOf="@id/icon"
android:layout_toEndOf="@id/icon"
android:orientation="vertical" >
<TextView
android:id="@+id/license"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:textSize="13sp" />
<TextView
android:id="@+id/categories"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/license"
android:layout_above="@id/status"
android:layout_centerVertical="true"
android:textSize="13sp" />
<TextView
android:id="@+id/status"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:textSize="13sp" />
</RelativeLayout>
</RelativeLayout>
</LinearLayout>
</ScrollView>
<ListView
android:id="@android:id/list"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_weight="0.5"
android:scrollbars="none" />
</LinearLayout>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:padding="5dp">
<fragment
android:id="@+id/fragment_app_list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:name="org.fdroid.fdroid.AppDetails$AppDetailsListFragment"
tools:layout="@android:layout/list_content"/>
</RelativeLayout>

View File

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="4dp"
android:paddingBottom="8dp"
android:baselineAligned="false"
android:orientation="vertical" >
<ImageView
android:id="@+id/icon"
android:contentDescription="@string/app_icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:padding="4dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
/>
<TextView
android:id="@+id/status"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/icon"
android:layout_toEndOf="@id/icon"
android:layout_alignBottom="@id/icon"
android:paddingLeft="4dp"
tools:text="Installed"
android:textSize="13sp" android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="false"
tools:text="F-Droid"
android:textSize="18sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:layout_toRightOf="@id/icon"
android:layout_above="@id/status" />
<TextView
android:id="@+id/categories"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_alignBottom="@id/icon"
android:paddingLeft="4dp"
tools:text="System, Internet"
android:textSize="13sp" android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/license"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_above="@id/categories"
android:paddingRight="4dp"
tools:text="GPLv3+"
android:textSize="13sp" />
<TextView
android:id="@+id/summary"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/icon"
android:textStyle="bold"
tools:text="Application manager" />
<TextView
android:id="@+id/appid"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/summary"
android:textSize="12sp"
tools:text="org.fdroid.fdroid" />
<TextView
android:id="@+id/signature"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/appid"
android:textSize="12sp" />
<TextView
android:id="@+id/antifeatures"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/signature"
android:layout_marginTop="6sp"
android:textStyle="bold"
android:textColor="#ff0000"
tools:text="Feeds you too much chocolate" />
<TextView
android:id="@+id/description"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/antifeatures"
android:layout_marginTop="8sp"
android:textSize="13sp"
android:singleLine="false"
tools:text="Connects to F-Droid compatible repositories. The default repo is hosted at f-droid.org, which contains only bona fide FOSS.
Android is open in the sense that you are free to install apks from anywhere you wish, but there are many good reasons for using a client/repository setup:
* Be notified when updates are available
* Keep track of older and beta versions
* Filter apps that aren't compatible with the device
* Find apps via categories and searchable descriptions
* Access associated urls for donations, source code etc.
* Stay safe by checking repo index signatures and apk hashes
Changelog" />
<TextView
android:id="@+id/permissions"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/description"
android:layout_marginTop="8sp"
android:singleLine="true"
android:textStyle="bold"
tools:text="Permissions for version 1.0" />
<TextView
android:id="@+id/permissions_list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/permissions"
android:textSize="13sp"
android:singleLine="false"
tools:text=" * Full network access
* View network connections
* View Wi-Fi connections
* Connect and disconnect from Wi-Fi
* Pair with Bluetooth devices
* Run at startup
* Modify or delete the contents of your USB storage
* Control Near Field Communication
* Directly install apps
* Delete apps
* Full permission to all device features and storage
* Test access to protected storage" />
</RelativeLayout>

View File

@ -1,104 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:padding="5dp"
android:baselineAligned="false"
android:orientation="vertical" >
<RelativeLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:baselineAligned="false" >
<ImageView
android:id="@+id/icon"
android:contentDescription="@string/app_icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_centerVertical="true"
android:padding="4dp"
android:scaleType="fitCenter"
/>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/icon"
android:layout_toEndOf="@id/icon"
android:padding="5dp"
android:baselineAligned="false"
android:orientation="vertical"
>
<TextView
android:id="@+id/license"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:singleLine="true"
android:ellipsize="end"
android:layout_marginLeft="6sp"
android:layout_marginStart="6sp"
android:textSize="12sp"
/>
<TextView
android:id="@+id/title"
android:textSize="17sp"
android:textStyle="bold"
android:singleLine="true"
android:ellipsize="end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:gravity="start"
android:textAlignment="viewStart"
android:layout_toLeftOf="@id/license"
android:layout_toStartOf="@id/license" />
<TextView
android:id="@+id/categories"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginLeft="6sp"
android:layout_marginStart="6sp"
android:layout_below="@id/title"
android:textSize="12sp" />
<TextView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:textSize="12sp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:gravity="start"
android:textAlignment="viewStart"
android:layout_toLeftOf="@id/categories"
android:layout_toStartOf="@id/categories"
android:layout_below="@id/title" />
</RelativeLayout>
</RelativeLayout>
<ListView
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scrollbars="none" />
</LinearLayout>

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="categorySpinner" />
<item type="id" name="appDetailsSummaryHeader" />
</resources>

View File

@ -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 <em>not</em> 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<Apk> {
class ApkListAdapter extends ArrayAdapter<Apk> {
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<Apk> 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<String> 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<String> 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);
}
}
}

View File

@ -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);
}

View File

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