Merge branch 'development' into experimental/refactor-update

Conflicts:
	src/org/fdroid/fdroid/RepoXMLHandler.java
	src/org/fdroid/fdroid/Utils.java
This commit is contained in:
Peter Serwylo 2014-01-05 19:22:30 +11:00
commit d80f2d58ad
28 changed files with 1578 additions and 845 deletions

View File

@ -112,6 +112,10 @@
</intent-filter>
</activity>
<activity
android:name=".views.RepoDetailsActivity"
android:label="@string/menu_manage" />
<activity
android:name=".AppDetails"
android:label="@string/app_details"

@ -1 +1 @@
Subproject commit 75ea560049c9a256ca4fba0a70de1971aa852612
Subproject commit 66042fe4a38d5e96030144546290ba0404d24e28

View File

@ -2,7 +2,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" >
android:orientation="vertical"
android:padding="6dp">
<TextView
android:layout_width="match_parent"

38
res/layout/repo_item.xml Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="25dp"
android:padding="8dp"
android:descendantFocusability="blocksDescendants">
<!--
descendantFocusability is here because if you have a child that responds
to touch events (in our case, the switch/toggle button) then the list item
itself will not respond to touch events.
http://syedasaraahmed.wordpress.com/2012/10/03/android-onitemclicklistener-not-responding-clickable-rowitem-of-custom-listview/
-->
<ImageView android:id="@+id/img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true" />
<TextView android:id="@+id/repo_name"
android:textSize="21sp"
android:textStyle="bold"
android:singleLine="true"
android:ellipsize="marquee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/img"
android:layout_alignParentLeft="true"/>
<TextView android:id="@+id/repo_unsigned"
android:textSize="14sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/repo_name"
android:text="@string/unsigned"
android:textColor="@color/unsigned"/>
</RelativeLayout>

131
res/layout/repodetails.xml Normal file
View File

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
android:paddingTop="@dimen/padding_top"
android:paddingLeft="@dimen/padding_side"
android:paddingRight="@dimen/padding_side">
<!-- Editable URL of this repo -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/label_repo_url"
android:text="@string/repo_url"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" />
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/input_repo_url"
android:inputType="textUri"
android:layout_below="@id/label_repo_url" />
<!-- Name of this repo -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/label_repo_name"
android:text="@string/repo_name"
android:layout_below="@id/input_repo_url"
android:layout_alignParentLeft="true"
android:paddingTop="@dimen/form_label_top"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="false"
android:id="@+id/text_repo_name"
android:layout_below="@id/label_repo_name" android:textStyle="bold"/>
<!-- Description - as pulled from the index file during last update... -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/label_description"
android:text="@string/repo_description"
android:layout_below="@id/text_repo_name"
android:layout_alignParentLeft="true"
android:paddingTop="@dimen/form_label_top"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="false"
android:scrollHorizontally="false"
android:id="@+id/text_description"
android:layout_below="@id/label_description" android:textStyle="bold"/>
<!-- Number of apps belonging to this repo -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/label_num_apps"
android:text="@string/repo_num_apps"
android:layout_below="@id/text_description"
android:layout_alignParentLeft="true"
android:paddingTop="@dimen/form_label_top" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/text_num_apps"
android:layout_below="@id/label_num_apps" android:textStyle="bold"/>
<!-- The last time this repo was updated -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/label_last_update"
android:text="@string/repo_last_update"
android:layout_below="@id/text_num_apps"
android:layout_alignParentLeft="true"
android:paddingTop="@dimen/form_label_top" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/text_last_update"
android:layout_below="@id/label_last_update" android:textStyle="bold"/>
<!-- Signature (or "unsigned" if none) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/label_signature"
android:text="@string/repo_signature"
android:layout_below="@id/text_last_update"
android:layout_alignParentLeft="true"
android:paddingTop="@dimen/form_label_top" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="false"
android:scrollHorizontally="false"
android:id="@+id/text_signature"
android:layout_below="@id/label_signature" android:textStyle="bold"/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="false"
android:scrollHorizontally="false"
android:id="@+id/text_signature_description"
android:layout_below="@id/text_signature"/>
<!-- The last time this repo was updated -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/text_not_yet_updated"
android:layout_below="@id/input_repo_url"
android:text="@string/repo_not_yet_updated"
android:textStyle="bold"
android:paddingTop="@dimen/form_label_top"/>
<Button
android:id="@+id/btn_update"
android:layout_centerHorizontal="true"
android:text="@string/repo_update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/text_not_yet_updated"/>
</RelativeLayout>

View File

@ -1,36 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/vw1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/vw1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView android:id="@+id/img"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView android:id="@+id/img"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView android:id="@+id/uri"
android:textSize="21sp"
android:textStyle="bold"
android:singleLine="true"
android:ellipsize="marquee"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
<TextView android:id="@+id/uri"
android:textSize="21sp"
android:textStyle="bold"
android:singleLine="true"
android:ellipsize="marquee"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
<TextView android:id="@+id/fingerprint"
android:textSize="14sp"
android:typeface="monospace"
android:singleLine="false"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
<TextView android:id="@+id/fingerprint"
android:textSize="14sp"
android:typeface="monospace"
android:singleLine="false"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!--

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/edit_repo"
android:title="@string/edit"
android:icon="@android:drawable/ic_menu_edit" />
<item
android:id="@+id/delete_repo"
android:title="@string/delete"
android:icon="@android:drawable/ic_menu_delete" />
</menu>

5
res/values/colors.xml Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="signed">#ffcccccc</color>
<color name="unsigned">#ffCC0000</color>
</resources>

6
res/values/dimens.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="padding_side">8dp</dimen>
<dimen name="padding_top">5dp</dimen>
<dimen name="form_label_top">5dp</dimen>
</resources>

View File

@ -7,10 +7,11 @@
<string name="installIncompatible">It seems like this package is not compatible with your device. Do you want to try and install it anyway?</string>
<string name="installDowngrade">You are trying to downgrade this application. Doing so might get it to malfunction and even lose your data. Do you want to try and downgrade it anyway?</string>
<string name="version">Version</string>
<string name="edit">Edit</string>
<string name="delete">Delete</string>
<string name="cache_downloaded">App cache</string>
<string name="cache_downloaded_on">Keep downloaded apk files on SD card</string>
<string name="cache_downloaded_off">Do not keep any apk files</string>
<string name="updates">Updates</string>
<string name="other">Other</string>
<string name="last_update_check">Last repo scan: %s</string>
@ -74,7 +75,7 @@
<string name="download_server">Getting application from</string>
<string name="repo_add_url">Repository address</string>
<string name="repo_add_fingerprint">fingerprint (optional)</string>
<string name="repo_add_fingerprint">Fingerprint (optional)</string>
<string name="repo_exists">This repo already exists!</string>
<string name="repo_exists_add_fingerprint">This repo is already setup, this will add new key information.</string>
<string name="repo_exists_enable">This repo is already setup, confirm that you want to re-enable it.</string>
@ -86,7 +87,7 @@
want to update them?</string>
<string name="menu_update_repo">Update Repos</string>
<string name="menu_manage">Manage Repos</string>
<string name="menu_manage">Repositories</string>
<string name="menu_preferences">Preferences</string>
<string name="menu_about">About</string>
<string name="menu_search">Search</string>
@ -163,6 +164,36 @@
<string name="compactlayout_on">Show icons at a smaller size</string>
<string name="compactlayout_off">Show icons at regular size</string>
<string name="theme">Theme</string>
<string name="unsigned">Unsigned</string>
<string name="repo_url">URL</string>
<string name="repo_num_apps"># of apps</string>
<string name="repo_signature">Signature</string>
<string name="repo_description">Description</string>
<string name="repo_last_update">Last update</string>
<string name="repo_update">Update</string>
<string name="repo_name">Name</string>
<string name="unsigned_description">This means that the list of
applications could not be verified. You should be careful
with applications downloaded from unsigned indexes.</string>
<string name="repo_not_yet_updated">This repository has not been used yet.
In order to view the apps it provides, you will need to update
it.\n\nOnce updated, the description and other details will
become available here.
</string>
<string name="repo_delete_details">Do you want to delete the \"{0}\"
repository, which has {1} apps in it? Any installed apps will NOT be
removed, but you will not be able to update them through F-Droid any
more.
</string>
<string name="unknown">Unknown</string>
<string name="repo_confirm_delete_title">Delete Repository?</string>
<string name="repo_confirm_delete_body">Deleting a repository means
apps from it will no longer be available from F-Droid.\n\nNote: All
previously installed apps will remain on your device.
</string>
<string name="repo_disabled_notification">Disabled "%1$s".\n\nYou will
need to re-enable this repository to install apps from it.
</string>
<string name="minsdk_or_later">Android %s or later</string>
</resources>

View File

@ -22,6 +22,9 @@ package org.fdroid.fdroid;
import android.annotation.SuppressLint;
import java.io.File;
import java.security.MessageDigest;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
@ -423,7 +426,7 @@ public class DB {
+ "name text, description text, inuse integer not null, "
+ "priority integer not null, pubkey text, fingerprint text, "
+ "maxage integer not null default 0, "
+ "lastetag text);";
+ "lastetag text, lastUpdated string);";
public static class Repo {
public int id;
@ -437,9 +440,107 @@ public class DB {
public String fingerprint; // always null for an unsigned repo
public int maxage; // maximum age of index that will be accepted - 0 for any
public String lastetag; // last etag we updated from, null forces update
public Date lastUpdated;
/**
* If we haven't run an update for this repo yet, then the name
* will be unknown, in which case we will just take a guess at an
* appropriate name based on the url (e.g. "fdroid.org/archive")
*/
public String getName() {
if (name == null) {
String tempName = null;
try {
URL url = new URL(address);
tempName = url.getHost() + url.getPath();
} catch (MalformedURLException e) {
tempName = address;
}
return tempName;
} else {
return name;
}
}
public String toString() {
return address;
}
public int getNumberOfApps() {
DB db = DB.getDB();
int count = db.countAppsForRepo(id);
DB.releaseDB();
return count;
}
/**
* @param application In order invalidate the list of apps, we require
* a reference to the top level application.
*/
public void enable(FDroidApp application) {
try {
DB db = DB.getDB();
List<DB.Repo> toEnable = new ArrayList<DB.Repo>(1);
toEnable.add(this);
db.enableRepos(toEnable);
} finally {
DB.releaseDB();
}
application.invalidateAllApps();
}
/**
* @param application See DB.Repo.enable(application)
*/
public void disable(FDroidApp application) {
disableRemove(application, false);
}
/**
* @param application See DB.Repo.enable(application)
*/
public void remove(FDroidApp application) {
disableRemove(application, true);
}
/**
* @param application See DB.Repo.enable(application)
*/
private void disableRemove(FDroidApp application, boolean removeAfterDisabling) {
try {
DB db = DB.getDB();
List<DB.Repo> toDisable = new ArrayList<DB.Repo>(1);
toDisable.add(this);
db.doDisableRepos(toDisable, removeAfterDisabling);
} finally {
DB.releaseDB();
}
application.invalidateAllApps();
}
public boolean isSigned() {
return this.pubkey != null && this.pubkey.length() > 0;
}
public boolean hasBeenUpdated() {
return this.lastetag != null;
}
}
private final int DBVersion = 34;
private final int DBVersion = 35;
private int countAppsForRepo(int id) {
String[] selection = { "COUNT(distinct id)" };
String[] selectionArgs = { Integer.toString(id) };
Cursor result = db.query(
TABLE_APK, selection, "repo = ?", selectionArgs, "repo", null, null);
if (result.getCount() > 0) {
result.moveToFirst();
return result.getInt(0);
} else {
return 0;
}
}
private static void createAppApk(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_APP);
@ -537,6 +638,9 @@ public class DB {
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.i("FDroid", "Upgrading database from v" + oldVersion + " v"
+ newVersion );
// Migrate repo list to new structure. (No way to change primary
// key in sqlite - table must be recreated)
if (oldVersion < 20) {
@ -580,8 +684,8 @@ public class DB {
ContentValues values = new ContentValues();
values.put("name", mContext.getString(R.string.default_repo_name));
values.put("description", mContext.getString(R.string.default_repo_description));
db.update(TABLE_REPO, values, "address = ?", new String[] {
mContext.getString(R.string.default_repo_address) });
db.update(TABLE_REPO, values, "address = ?", new String[]{
mContext.getString(R.string.default_repo_address)});
values.clear();
values.put("name", mContext.getString(R.string.default_repo_name2));
values.put("description", mContext.getString(R.string.default_repo_description2));
@ -615,10 +719,15 @@ public class DB {
if (oldVersion < 30) {
db.execSQL("alter table " + TABLE_REPO + " add column maxage integer not null default 0");
}
if (oldVersion < 34) {
if (oldVersion < 33) {
db.execSQL("alter table " + TABLE_REPO + " add column version integer not null default 0");
}
if (oldVersion < 35) {
if (!columnExists(db, TABLE_REPO, "lastUpdated"))
db.execSQL("Alter table " + TABLE_REPO + " add column lastUpdated string");
}
}
}
@ -1289,7 +1398,7 @@ public class DB {
try {
c = db.query(TABLE_REPO, new String[] { "address", "name",
"description", "version", "inuse", "priority", "pubkey",
"fingerprint", "maxage", "lastetag" },
"fingerprint", "maxage", "lastetag", "lastUpdated" },
"id = ?", new String[] { Integer.toString(id) }, null, null, null);
if (!c.moveToFirst())
return null;
@ -1305,6 +1414,13 @@ public class DB {
repo.fingerprint = c.getString(7);
repo.maxage = c.getInt(8);
repo.lastetag = c.getString(9);
try {
repo.lastUpdated = c.getString(10) != null ?
mDateFormat.parse( c.getString(10)) :
null;
} catch (ParseException e) {
Log.e("FDroid", "Error parsing date " + c.getString(10));
}
return repo;
} finally {
if (c != null)
@ -1347,6 +1463,27 @@ public class DB {
return repos;
}
public void enableRepos(List<DB.Repo> repos) {
if (repos.isEmpty()) return;
ContentValues values = new ContentValues(1);
values.put("inuse", 1);
String[] whereArgs = new String[repos.size()];
StringBuilder where = new StringBuilder("address IN (");
for (int i = 0; i < repos.size(); i ++) {
Repo repo = repos.get(i);
repo.inuse = true;
whereArgs[i] = repo.address;
where.append('?');
if ( i < repos.size() - 1 ) {
where.append(',');
}
}
where.append(")");
db.update(TABLE_REPO, values, where.toString(), whereArgs);
}
public void changeServerStatus(String address) {
db.execSQL("update " + TABLE_REPO
+ " set inuse=1-inuse, lastetag=null where address = ?",
@ -1361,8 +1498,17 @@ public class DB {
}
public void updateRepoByAddress(Repo repo) {
updateRepo(repo, "address", repo.address);
}
public void updateRepo(Repo repo) {
updateRepo(repo, "id", repo.id + "");
}
private void updateRepo(Repo repo, String field, String value) {
ContentValues values = new ContentValues();
values.put("name", repo.name);
values.put("address", repo.address);
values.put("description", repo.description);
values.put("version", repo.version);
values.put("inuse", repo.inuse);
@ -1376,13 +1522,24 @@ public class DB {
}
values.put("maxage", repo.maxage);
values.put("lastetag", (String) null);
db.update(TABLE_REPO, values, "address = ?",
new String[] { repo.address });
db.update(TABLE_REPO, values, field + " = ?",
new String[] { value });
}
/**
* Updates the lastUpdated time for every enabled repo.
*/
public void refreshLastUpdates() {
ContentValues values = new ContentValues();
values.put("lastUpdated", mDateFormat.format(new Date()));
db.update(TABLE_REPO, values, "inuse = 1",
new String[] {});
}
public void writeLastEtag(Repo repo) {
ContentValues values = new ContentValues();
values.put("lastetag", repo.lastetag);
values.put("lastUpdated", mDateFormat.format(new Date()));
db.update(TABLE_REPO, values, "address = ?",
new String[] { repo.address });
}
@ -1415,18 +1572,23 @@ public class DB {
db.insert(TABLE_REPO, null, values);
}
public void doDisableRepos(List<String> addresses, boolean remove) {
if (addresses.isEmpty()) return;
public void doDisableRepos(List<Repo> repos, boolean remove) {
if (repos.isEmpty()) return;
db.beginTransaction();
try {
for (String address : addresses) {
// TODO: Replace with
// "delete from apk join repo where repo in (?, ?, ...)
// "update repo set inuse = 0 | delete from repo ] where repo in (?, ?, ...)
try {
for (Repo repo : repos) {
String address = repo.address;
// Before removing the repo, remove any apks that are
// connected to it...
Cursor c = null;
try {
c = db.query(TABLE_REPO, new String[] { "id" },
"address = ?", new String[] { address },
c = db.query(TABLE_REPO, new String[]{"id"},
"address = ?", new String[]{address},
null, null, null, null);
c.moveToFirst();
if (!c.isAfterLast()) {
@ -1441,6 +1603,13 @@ public class DB {
if (remove)
db.delete(TABLE_REPO, "address = ?",
new String[] { address });
else {
ContentValues values = new ContentValues(2);
values.put("inuse", 0);
values.put("lastetag", (String)null);
db.update(TABLE_REPO, values, "address = ?",
new String[] { address });
}
}
List<App> apps = getApps(false);
for (App app : apps) {

View File

@ -27,13 +27,10 @@ import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.NotificationManager;
import android.app.ProgressDialog;
import android.content.pm.PackageInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import android.support.v4.app.FragmentActivity;
import android.support.v4.view.ViewPager;
import android.util.Log;
@ -60,8 +57,6 @@ public class FDroid extends FragmentActivity {
private static final int ABOUT = Menu.FIRST + 3;
private static final int SEARCH = Menu.FIRST + 4;
private ProgressDialog pd;
private ViewPager viewPager;
private AppListManager manager = null;
@ -132,11 +127,6 @@ public class FDroid extends FragmentActivity {
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuItem update = menu.add(Menu.NONE, UPDATE_REPO, 1, R.string.menu_update_repo).setIcon(
android.R.drawable.ic_menu_rotate);
MenuItemCompat.setShowAsAction(update,
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
menu.add(Menu.NONE, MANAGE_REPO, 2, R.string.menu_manage).setIcon(
android.R.drawable.ic_menu_agenda);
MenuItem search = menu.add(Menu.NONE, SEARCH, 3, R.string.menu_search).setIcon(
@ -236,7 +226,7 @@ public class FDroid extends FragmentActivity {
case REQUEST_APPDETAILS:
break;
case REQUEST_MANAGEREPOS:
if (data.hasExtra("update")) {
if (data.hasExtra(ManageRepo.REQUEST_UPDATE)) {
AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
ask_alrt.setTitle(getString(R.string.repo_update_title));
ask_alrt.setIcon(android.R.drawable.ic_menu_rotate);
@ -246,7 +236,7 @@ public class FDroid extends FragmentActivity {
@Override
public void onClick(DialogInterface dialog,
int whichButton) {
updateRepos();
updateRepos();
}
});
ask_alrt.setNegativeButton(getString(R.string.no),
@ -254,6 +244,7 @@ public class FDroid extends FragmentActivity {
@Override
public void onClick(DialogInterface dialog,
int whichButton) {
// do nothing
}
});
AlertDialog alert = ask_alrt.create();
@ -298,34 +289,6 @@ public class FDroid extends FragmentActivity {
});
}
// For receiving results from the UpdateService when we've told it to
// update in response to a user request.
private class UpdateReceiver extends ResultReceiver {
public UpdateReceiver(Handler handler) {
super(handler);
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
String message = resultData.getString(UpdateService.RESULT_MESSAGE);
boolean finished = false;
if (resultCode == UpdateService.STATUS_ERROR) {
Toast.makeText(FDroid.this, message, Toast.LENGTH_LONG).show();
finished = true;
} else if (resultCode == UpdateService.STATUS_CHANGES) {
repopulateViews();
finished = true;
} else if (resultCode == UpdateService.STATUS_SAME) {
finished = true;
} else if (resultCode == UpdateService.STATUS_INFO) {
pd.setMessage(message);
}
if (finished && pd.isShowing())
pd.dismiss();
}
}
/**
* The first time the app is run, we will have an empty app list.
* If this is the case, we will attempt to update with the default repo.
@ -351,16 +314,14 @@ public class FDroid extends FragmentActivity {
// is told to do the update, which will result in the database changing. The
// UpdateReceiver class should get told when this is finished.
public void updateRepos() {
pd = ProgressDialog.show(this, getString(R.string.process_wait_title),
getString(R.string.process_update_msg), true, true);
pd.setIcon(android.R.drawable.ic_dialog_info);
pd.setCanceledOnTouchOutside(false);
Intent intent = new Intent(this, UpdateService.class);
UpdateReceiver mUpdateReceiver = new UpdateReceiver(new Handler());
intent.putExtra("receiver", mUpdateReceiver);
startService(intent);
UpdateService.updateNow(this).setListener(new ProgressListener() {
@Override
public void onProgress(Event event) {
if (event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES){
repopulateViews();
}
}
});
}
private TabManager getTabManager() {

View File

@ -19,72 +19,59 @@
package org.fdroid.fdroid;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.ListActivity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
import android.support.v4.view.MenuItemCompat;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.*;
import org.fdroid.fdroid.DB.Repo;
import org.fdroid.fdroid.compat.ActionBarCompat;
import org.fdroid.fdroid.compat.ClipboardCompat;
import org.fdroid.fdroid.views.RepoAdapter;
import org.fdroid.fdroid.views.RepoDetailsActivity;
import org.fdroid.fdroid.views.fragments.RepoDetailsFragment;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
public class ManageRepo extends ListActivity {
private final int ADD_REPO = 1;
private final int REM_REPO = 2;
private static final String DEFAULT_NEW_REPO_TEXT = "https://";
private final int ADD_REPO = 1;
private final int UPDATE_REPOS = 2;
private boolean changed = false;
/**
* If we have a new repo added, or the address of a repo has changed, then
* we when we're finished, we'll set this boolean to true in the intent
* that we finish with, to signify that we want the main list of apps
* updated.
*/
public static final String REQUEST_UPDATE = "update";
private enum PositiveAction {
ADD_NEW, ENABLE, IGNORE
}
private PositiveAction positiveAction;
private List<DB.Repo> repos;
private boolean changed = false;
private static List<String> reposToDisable;
private static List<String> reposToRemove;
private RepoAdapter repoAdapter;
public void disableRepo(String address) {
if (reposToDisable.contains(address)) return;
reposToDisable.add(address);
}
public void removeRepo(String address) {
if (reposToRemove.contains(address)) return;
reposToRemove.add(address);
}
public void removeRepos(List<String> addresses) {
for (String address : addresses)
removeRepo(address);
}
/**
* True if activity started with an intent such as from QR code. False if
* opened from, e.g. the main menu.
*/
private boolean isImportingRepo = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -92,11 +79,15 @@ public class ManageRepo extends ListActivity {
((FDroidApp) getApplication()).applyTheme(this);
super.onCreate(savedInstanceState);
ActionBarCompat abCompat = ActionBarCompat.create(this);
abCompat.setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.repolist);
repoAdapter = new RepoAdapter(this);
setListAdapter(repoAdapter);
/*
TODO: Find some other way to display this info, now that we use the ListView widgets...
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(getBaseContext());
@ -112,8 +103,7 @@ public class ManageRepo extends ListActivity {
}
tv_lastCheck.setText(getString(R.string.last_update_check,s_lastUpdateCheck));
reposToRemove = new ArrayList<String>();
reposToDisable = new ArrayList<String>();
*/
/* let's see if someone is trying to send us a new repo */
Intent intent = getIntent();
@ -126,6 +116,9 @@ public class ManageRepo extends ListActivity {
String host = uri.getHost().toLowerCase(Locale.ENGLISH);
if (scheme.equals("fdroidrepos") || scheme.equals("fdroidrepo")
|| scheme.equals("https") || scheme.equals("http")) {
isImportingRepo = true;
// QRCode are more efficient in all upper case, so some incoming
// URLs might be encoded in all upper case. Therefore, we allow
// the standard paths to be encoded all upper case, then they'll
@ -147,81 +140,95 @@ public class ManageRepo extends ListActivity {
@Override
protected void onResume() {
super.onResume();
redraw();
}
private void redraw() {
try {
DB db = DB.getDB();
repos = db.getRepos();
} finally {
DB.releaseDB();
}
List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
Map<String, Object> server_line;
for (DB.Repo repo : repos) {
server_line = new HashMap<String, Object>();
server_line.put("address", repo.address);
if (repo.inuse) {
server_line.put("inuse", R.drawable.btn_check_on);
} else {
server_line.put("inuse", R.drawable.btn_check_off);
}
if (repo.fingerprint != null) {
server_line.put("fingerprint", repo.fingerprint);
}
result.add(server_line);
}
SimpleAdapter show_out = new SimpleAdapter(this, result,
R.layout.repolisticons, new String[] { "address", "inuse",
"fingerprint" }, new int[] { R.id.uri, R.id.img,
R.id.fingerprint });
setListAdapter(show_out);
refreshList();
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
try {
DB db = DB.getDB();
String address = repos.get(position).address;
db.changeServerStatus(address);
// TODO: Disabling and re-enabling a repo will delete its apks too.
disableRepo(address);
} finally {
DB.releaseDB();
}
changed = true;
redraw();
DB.Repo repo = (DB.Repo)getListView().getItemAtPosition(position);
editRepo(repo);
}
private void refreshList() {
repoAdapter.refresh();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuItem item = menu.add(Menu.NONE, ADD_REPO, 1, R.string.menu_add_repo).setIcon(
android.R.drawable.ic_menu_add);
menu.add(Menu.NONE, REM_REPO, 2, R.string.menu_rem_repo).setIcon(
android.R.drawable.ic_menu_close_clear_cancel);
MenuItemCompat.setShowAsAction(item,
MenuItemCompat.SHOW_AS_ACTION_IF_ROOM |
MenuItem updateItem = menu.add(Menu.NONE, UPDATE_REPOS, 1,
R.string.menu_update_repo).setIcon(R.drawable.ic_menu_refresh);
MenuItemCompat.setShowAsAction(updateItem,
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
MenuItem addItem = menu.add(Menu.NONE, ADD_REPO, 1, R.string.menu_add_repo).setIcon(
android.R.drawable.ic_menu_add);
MenuItemCompat.setShowAsAction(addItem,
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
return true;
}
protected void addRepo(String repoUri, String fingerprint) {
public static final int SHOW_REPO_DETAILS = 1;
public void editRepo(DB.Repo repo) {
Log.d("FDroid", "Showing details screen for repo: '" + repo + "'.");
Intent intent = new Intent(this, RepoDetailsActivity.class);
intent.putExtra(RepoDetailsFragment.ARG_REPO_ID, repo.id);
startActivityForResult(intent, SHOW_REPO_DETAILS);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == SHOW_REPO_DETAILS && resultCode == RESULT_OK) {
boolean wasDeleted = data.getBooleanExtra(RepoDetailsActivity.ACTION_IS_DELETED, false);
boolean wasEnabled = data.getBooleanExtra(RepoDetailsActivity.ACTION_IS_ENABLED, false);
boolean wasDisabled = data.getBooleanExtra(RepoDetailsActivity.ACTION_IS_DISABLED, false);
boolean wasChanged = data.getBooleanExtra(RepoDetailsActivity.ACTION_IS_CHANGED, false);
if (wasDeleted) {
int repoId = data.getIntExtra(RepoDetailsActivity.DATA_REPO_ID, 0);
remove(repoId);
} else if (wasEnabled || wasDisabled || wasChanged) {
changed = true;
}
}
}
private DB.Repo getRepoById(int repoId) {
for (int i = 0; i < getListAdapter().getCount(); i ++) {
DB.Repo repo = (DB.Repo)getListAdapter().getItem(i);
if (repo.id == repoId) {
return repo;
}
}
return null;
}
private void remove(int repoId) {
DB.Repo repo = getRepoById(repoId);
if (repo == null) {
return;
}
List<DB.Repo> reposToRemove = new ArrayList<DB.Repo>(1);
reposToRemove.add(repo);
try {
DB db = DB.getDB();
db.addRepo(repoUri, null, null, 0, 10, null, fingerprint, 0, true);
db.doDisableRepos(reposToRemove, true);
} finally {
DB.releaseDB();
}
refreshList();
}
protected List<Repo> getRepos() {
@ -253,16 +260,30 @@ public class ManageRepo extends ListActivity {
return super.onOptionsItemSelected(item);
}
private void updateRepos() {
UpdateService.updateNow(this).setListener(new ProgressListener() {
@Override
public void onProgress(Event event) {
// No need to prompt to update any more, we just did it!
changed = false;
refreshList();
}
});
}
private void showAddRepo() {
showAddRepo(getNewRepoUri(), null);
}
private void showAddRepo(String newAddress, String newFingerprint) {
LayoutInflater li = LayoutInflater.from(this);
View view = li.inflate(R.layout.addrepo, null);
Builder p = new AlertDialog.Builder(this).setView(view);
final AlertDialog alrt = p.create();
View view = getLayoutInflater().inflate(R.layout.addrepo, null);
final AlertDialog alrt = new AlertDialog.Builder(this).setView(view).create();
final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri);
final EditText fingerprintEditText = (EditText) view.findViewById(R.id.edit_fingerprint);
List<Repo> repos = getRepos();
final Repo repo = getRepoByAddress(newAddress, repos);
final Repo repo = newAddress != null && isImportingRepo ? getRepoByAddress(newAddress, repos) : null;
alrt.setIcon(android.R.drawable.ic_menu_add);
alrt.setTitle(getString(R.string.repo_add_title));
@ -271,15 +292,18 @@ public class ManageRepo extends ListActivity {
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String fp = fingerprintEditText.getText().toString();
// the DB uses null for no fingerprint but the above
// code returns "" rather than null if its blank
if (fp.equals(""))
fp = null;
if (positiveAction == PositiveAction.ADD_NEW)
addRepoPositiveAction(uriEditText.getText().toString(), fp, null);
createNewRepo(uriEditText.getText().toString(), fp);
else if (positiveAction == PositiveAction.ENABLE)
addRepoPositiveAction(null, null, repo);
createNewRepo(repo);
}
});
@ -290,6 +314,7 @@ public class ManageRepo extends ListActivity {
public void onClick(DialogInterface dialog, int which) {
setResult(Activity.RESULT_CANCELED);
finish();
return;
}
});
alrt.show();
@ -332,29 +357,58 @@ public class ManageRepo extends ListActivity {
}
}
if (newAddress != null)
uriEditText.setText(newAddress);
if (newFingerprint != null)
fingerprintEditText.setText(newFingerprint);
if (newAddress != null) {
// This trick of emptying text then appending,
// rather than just setting in the first place,
// is neccesary to move the cursor to the end of the input.
uriEditText.setText("");
uriEditText.append(newAddress);
}
}
private void addRepoPositiveAction(String address, String fingerprint, Repo repo) {
if (address != null) {
addRepo(address, fingerprint);
} else if (repo != null) {
// force-enable an existing repo
repo.inuse = true;
try {
DB db = DB.getDB();
db.updateRepoByAddress(repo);
} finally {
DB.releaseDB();
}
/**
* Adds a new repo to the database.
*/
private void createNewRepo(String address, String fingerprint) {
try {
DB db = DB.getDB();
db.addRepo(address, null, null, 0, 10, null, fingerprint, 0, true);
} finally {
DB.releaseDB();
}
finishedAddingRepo();
}
/**
* Seeing as this repo already exists, we will force it to be enabled again.
*/
private void createNewRepo(Repo repo) {
repo.inuse = true;
try {
DB db = DB.getDB();
db.updateRepoByAddress(repo);
} finally {
DB.releaseDB();
}
finishedAddingRepo();
}
/**
* If started by an intent that expects a result (e.g. QR codes) then we
* will set a result and finish. Otherwise, we'll refresh the list of
* repos to reflect the newly created repo.
*/
private void finishedAddingRepo() {
changed = true;
redraw();
setResult(Activity.RESULT_OK);
finish();
if (isImportingRepo) {
setResult(Activity.RESULT_OK);
finish();
} else {
refreshList();
}
}
@Override
@ -362,89 +416,77 @@ public class ManageRepo extends ListActivity {
super.onMenuItemSelected(featureId, item);
switch (item.getItemId()) {
case ADD_REPO:
showAddRepo(null, null);
if (item.getItemId() == ADD_REPO) {
showAddRepo();
return true;
case REM_REPO:
final List<String> rem_lst = new ArrayList<String>();
CharSequence[] b = new CharSequence[repos.size()];
for (int i = 0; i < repos.size(); i++) {
b[i] = repos.get(i).address;
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.repo_delete_title));
builder.setIcon(android.R.drawable.ic_menu_close_clear_cancel);
builder.setMultiChoiceItems(b, null,
new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog,
int whichButton, boolean isChecked) {
if (isChecked) {
rem_lst.add(repos.get(whichButton).address);
} else {
rem_lst.remove(repos.get(whichButton).address);
}
}
});
builder.setPositiveButton(getString(R.string.ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int whichButton) {
try {
DB db = DB.getDB();
removeRepos(rem_lst);
} finally {
DB.releaseDB();
}
changed = true;
redraw();
}
});
builder.setNegativeButton(getString(R.string.cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int whichButton) {
}
});
AlertDialog alert = builder.create();
alert.show();
} else if (item.getItemId() == UPDATE_REPOS) {
updateRepos();
return true;
}
return true;
return false;
}
/**
* If there is text in the clipboard, and it looks like a URL, use that.
* Otherwise return "https://".
*/
private String getNewRepoUri() {
ClipboardCompat clipboard = ClipboardCompat.create(this);
String text = clipboard.getText();
if (text != null) {
try {
new URL(text);
} catch (MalformedURLException e) {
text = null;
}
}
if (text == null) {
text = DEFAULT_NEW_REPO_TEXT;
}
return text;
}
@Override
public void finish() {
if (!reposToRemove.isEmpty()) {
try {
DB db = DB.getDB();
db.doDisableRepos(reposToRemove, true);
} finally {
DB.releaseDB();
}
((FDroidApp) getApplication()).invalidateAllApps();
}
if (!reposToDisable.isEmpty()) {
try {
DB db = DB.getDB();
db.doDisableRepos(reposToDisable, false);
} finally {
DB.releaseDB();
}
((FDroidApp) getApplication()).invalidateAllApps();
}
Intent ret = new Intent();
if (changed)
ret.putExtra("update", true);
this.setResult(RESULT_OK, ret);
if (changed) {
Log.i("FDroid", "Repo details have changed, prompting for update.");
ret.putExtra(REQUEST_UPDATE, true);
}
setResult(RESULT_OK, ret);
super.finish();
}
/**
* NOTE: If somebody toggles a repo off then on again, it will have removed
* all apps from the index when it was toggled off, so when it is toggled on
* again, then it will require a refresh.
*
* Previously, I toyed with the idea of remembering whether they had
* toggled on or off, and then only actually performing the function when
* the activity stopped, but I think that will be problematic. What about
* when they press the home button, or edit a repos details? It will start
* to become somewhat-random as to when the actual enabling, disabling is
* performed.
*
* So now, it just does the disable as soon as the user clicks "Off" and
* then removes the apps. To compensate for the removal of apps from
* index, it notifies the user via a toast that the apps have been removed.
* Also, as before, it will still prompt the user to update the repos if
* you toggled on on.
*/
public void setRepoEnabled(DB.Repo repo, boolean enabled) {
FDroidApp app = (FDroidApp)getApplication();
if (enabled) {
repo.enable(app);
changed = true;
} else {
repo.disable(app);
String notification = getString(R.string.repo_disabled_notification, repo.toString());
Toast.makeText(this, notification, Toast.LENGTH_LONG).show();
}
}
}

View File

@ -1,6 +1,8 @@
package org.fdroid.fdroid;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
public interface ProgressListener {
@ -9,7 +11,7 @@ public interface ProgressListener {
// I went a bit overboard with the overloaded constructors, but they all
// seemed potentially useful and unambiguous, so I just put them in there
// while I'm here.
public static class Event {
public static class Event implements Parcelable {
public static final int NO_VALUE = Integer.MIN_VALUE;
@ -49,6 +51,30 @@ public interface ProgressListener {
this.total = total;
this.data = data == null ? new Bundle() : data;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(type);
dest.writeInt(progress);
dest.writeInt(total);
dest.writeBundle(data);
}
public static final Parcelable.Creator<Event> CREATOR = new Parcelable.Creator<Event>() {
public Event createFromParcel(Parcel in) {
return new Event(in.readInt(), in.readInt(), in.readInt(), in.readBundle());
}
public Event[] newArray(int size) {
return new Event[size];
}
};
}
}

View File

@ -19,63 +19,17 @@
package org.fdroid.fdroid;
<<<<<<< HEAD
import android.os.Bundle;
import org.fdroid.fdroid.updater.RepoUpdater;
||||||| merged common ancestors
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.Certificate;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.net.ssl.SSLHandshakeException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
=======
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.Certificate;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.net.ssl.SSLHandshakeException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
>>>>>>> master
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class RepoXMLHandler extends DefaultHandler {
@ -89,10 +43,10 @@ public class RepoXMLHandler extends DefaultHandler {
private DB.Apk curapk = null;
private StringBuilder curchars = new StringBuilder();
// After processing the XML, these will be null if the index didn't specify
// After processing the XML, these will be -1 if the index didn't specify
// them - otherwise it will be the value specified.
private String version;
private String maxage;
private int version = -1;
private int maxage = -1;
// After processing the XML, this will be null if the index specified a
// public key - otherwise a public key. This is used for TOFU where an
@ -124,17 +78,13 @@ public class RepoXMLHandler extends DefaultHandler {
progressListener = listener;
}
public int getMaxAge() {
int age = 0;
if (maxage != null) {
try {
age = Integer.parseInt(maxage);
} catch (NumberFormatException e) {
// Do nothing...
}
}
return age;
}
public int getMaxAge() { return maxage; }
public int getVersion() { return version; }
public String getDescription() { return description; }
public String getName() { return name; }
public String getPubKey() {
return pubkey;
@ -286,6 +236,8 @@ public class RepoXMLHandler extends DefaultHandler {
} else if (curel.equals("requirements")) {
curapp.requirements = DB.CommaSeparatedList.make(str);
}
} else if (curel.equals("description")) {
description = str;
}
}
@ -298,8 +250,21 @@ public class RepoXMLHandler extends DefaultHandler {
String pk = attributes.getValue("", "pubkey");
if (pk != null)
pubkey = pk;
version = attributes.getValue("", "version");
maxage = attributes.getValue("", "maxage");
String maxAgeAttr = attributes.getValue("", "maxage");
if (maxAgeAttr != null) {
try {
maxage = Integer.parseInt(maxAgeAttr);
} catch (NumberFormatException nfe) {}
}
String versionAttr = attributes.getValue("", "version");
if (versionAttr != null) {
try {
version = Integer.parseInt(versionAttr);
} catch (NumberFormatException nfe) {}
}
String nm = attributes.getValue("", "name");
if (nm != null)
name = nm;
@ -330,469 +295,6 @@ public class RepoXMLHandler extends DefaultHandler {
curchars.setLength(0);
}
<<<<<<< HEAD
||||||| merged common ancestors
// Get a remote file. Returns the HTTP response code.
// If 'etag' is not null, it's passed to the server as an If-None-Match
// header, in which case expect a 304 response if nothing changed.
// In the event of a 200 response ONLY, 'retag' (which should be passed
// empty) may contain an etag value for the response, or it may be left
// empty if none was available.
private static int getRemoteFile(Context ctx, String url, String dest,
String etag, StringBuilder retag,
ProgressListener progressListener,
ProgressListener.Event progressEvent) throws MalformedURLException,
IOException {
long startTime = System.currentTimeMillis();
URL u = new URL(url);
HttpURLConnection connection = (HttpURLConnection) u.openConnection();
if (etag != null)
connection.setRequestProperty("If-None-Match", etag);
int code = connection.getResponseCode();
if (code == 200) {
// Testing in the emulator for me, showed that figuring out the filesize took about 1 to 1.5 seconds.
// To put this in context, downloading a repo of:
// - 400k takes ~6 seconds
// - 5k takes ~3 seconds
// on my connection. I think the 1/1.5 seconds is worth it, because as the repo grows, the tradeoff will
// become more worth it.
progressEvent.total = connection.getContentLength();
Log.d("FDroid", "Downloading " + progressEvent.total + " bytes from " + url);
InputStream input = null;
OutputStream output = null;
try {
input = connection.getInputStream();
output = ctx.openFileOutput(dest, Context.MODE_PRIVATE);
Utils.copy(input, output, progressListener, progressEvent);
} finally {
Utils.closeQuietly(output);
Utils.closeQuietly(input);
}
String et = connection.getHeaderField("ETag");
if (et != null)
retag.append(et);
}
Log.d("FDroid", "Fetched " + url + " (" + progressEvent.total +
" bytes) in " + (System.currentTimeMillis() - startTime) +
"ms");
return code;
}
// Do an update from the given repo. All applications found, and their
// APKs, are added to 'apps'. (If 'apps' already contains an app, its
// APKs are merged into the existing one).
// Returns null if successful, otherwise an error message to be displayed
// to the user (if there is an interactive user!)
// 'newetag' should be passed empty. On success, it may contain an etag
// value for the index that was successfully processed, or it may contain
// null if none was available.
public static String doUpdate(Context ctx, DB.Repo repo,
List<DB.App> apps, StringBuilder newetag, List<Integer> keeprepos,
ProgressListener progressListener) {
try {
int code = 0;
if (repo.pubkey != null) {
// This is a signed repo - we download the jar file,
// check the signature, and extract the index...
Log.d("FDroid", "Getting signed index from " + repo.address + " at " +
logDateFormat.format(new Date(System.currentTimeMillis())));
String address = repo.address + "/index.jar?"
+ ctx.getString(R.string.version_name);
Bundle progressData = createProgressData(repo.address);
ProgressListener.Event event = new ProgressListener.Event(
RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD, progressData);
code = getRemoteFile(ctx, address, "tempindex.jar",
repo.lastetag, newetag, progressListener, event );
if (code == 200) {
String jarpath = ctx.getFilesDir() + "/tempindex.jar";
JarFile jar = null;
JarEntry je;
Certificate[] certs;
try {
jar = new JarFile(jarpath, true);
je = (JarEntry) jar.getEntry("index.xml");
File efile = new File(ctx.getFilesDir(),
"/tempindex.xml");
InputStream input = null;
OutputStream output = null;
try {
input = jar.getInputStream(je);
output = new FileOutputStream(efile);
Utils.copy(input, output);
} finally {
Utils.closeQuietly(output);
Utils.closeQuietly(input);
}
certs = je.getCertificates();
} catch (SecurityException e) {
Log.e("FDroid", "Invalid hash for index file");
return "Invalid hash for index file";
} finally {
if (jar != null) {
jar.close();
}
}
if (certs == null) {
Log.d("FDroid", "No signature found in index");
return "No signature found in index";
}
Log.d("FDroid", "Index has " + certs.length + " signature"
+ (certs.length > 1 ? "s." : "."));
boolean match = false;
for (Certificate cert : certs) {
String certdata = Hasher.hex(cert.getEncoded());
if (repo.pubkey.equals(certdata)) {
match = true;
break;
}
}
if (!match) {
Log.d("FDroid", "Index signature mismatch");
return "Index signature mismatch";
}
}
} else {
// It's an old-fashioned unsigned repo...
Log.d("FDroid", "Getting unsigned index from " + repo.address);
Bundle eventData = createProgressData(repo.address);
ProgressListener.Event event = new ProgressListener.Event(
RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD, eventData);
code = getRemoteFile(ctx, repo.address + "/index.xml",
"tempindex.xml", repo.lastetag, newetag,
progressListener, event);
}
if (code == 200) {
// Process the index...
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser sp = spf.newSAXParser();
XMLReader xr = sp.getXMLReader();
RepoXMLHandler handler = new RepoXMLHandler(repo, apps, progressListener);
xr.setContentHandler(handler);
File tempIndex = new File(ctx.getFilesDir() + "/tempindex.xml");
BufferedReader r = new BufferedReader(new FileReader(tempIndex));
// A bit of a hack, this might return false positives if an apps description
// or some other part of the XML file contains this, but it is a pretty good
// estimate and makes the progress counter more informative.
// As with asking the server about the size of the index before downloading,
// this also has a time tradeoff. It takes about three seconds to iterate
// through the file and count 600 apps on a slow emulator (v17), but if it is
// taking two minutes to update, the three second wait may be worth it.
final String APPLICATION = "<application";
handler.setTotalAppCount(Utils.countSubstringOccurrence(tempIndex, APPLICATION));
InputSource is = new InputSource(r);
xr.parse(is);
if (handler.pubkey != null && repo.pubkey == null) {
// We read an unsigned index, but that indicates that
// a signed version is now available...
Log.d("FDroid",
"Public key found - switching to signed repo for future updates");
repo.pubkey = handler.pubkey;
try {
DB db = DB.getDB();
db.updateRepoByAddress(repo);
} finally {
DB.releaseDB();
}
}
if (handler.maxage != null) {
int maxage = Integer.parseInt(handler.maxage);
if (maxage != repo.maxage) {
Log.d("FDroid",
"Repo specified a new maximum age - updated");
repo.maxage = maxage;
try {
DB db = DB.getDB();
db.updateRepoByAddress(repo);
} finally {
DB.releaseDB();
}
}
}
} else if (code == 304) {
// The index is unchanged since we last read it. We just mark
// everything that came from this repo as being updated.
Log.d("FDroid", "Repo index for " + repo.address
+ " is up to date (by etag)");
keeprepos.add(repo.id);
// Make sure we give back the same etag. (The 200 route will
// have supplied a new one.
newetag.append(repo.lastetag);
} else {
return "Failed to read index - HTTP response "
+ Integer.toString(code);
}
} catch (SSLHandshakeException sslex) {
Log.e("FDroid", "SSLHandShakeException updating from "
+ repo.address + ":\n" + Log.getStackTraceString(sslex));
return "A problem occurred while establishing an SSL connection. If this problem persists, AND you have a very old device, you could try using http instead of https for the repo URL.";
} catch (Exception e) {
Log.e("FDroid", "Exception updating from " + repo.address + ":\n"
+ Log.getStackTraceString(e));
return "Failed to update - " + e.getMessage();
} finally {
ctx.deleteFile("tempindex.xml");
ctx.deleteFile("tempindex.jar");
}
return null;
}
=======
// Get a remote file. Returns the HTTP response code.
// If 'etag' is not null, it's passed to the server as an If-None-Match
// header, in which case expect a 304 response if nothing changed.
// In the event of a 200 response ONLY, 'retag' (which should be passed
// empty) may contain an etag value for the response, or it may be left
// empty if none was available.
private static int getRemoteFile(Context ctx, String url, String dest,
String etag, StringBuilder retag,
ProgressListener progressListener,
ProgressListener.Event progressEvent) throws MalformedURLException,
IOException {
long startTime = System.currentTimeMillis();
URL u = new URL(url);
HttpURLConnection connection = (HttpURLConnection) u.openConnection();
if (etag != null)
connection.setRequestProperty("If-None-Match", etag);
int code = connection.getResponseCode();
if (code == 200) {
// Testing in the emulator for me, showed that figuring out the filesize took about 1 to 1.5 seconds.
// To put this in context, downloading a repo of:
// - 400k takes ~6 seconds
// - 5k takes ~3 seconds
// on my connection. I think the 1/1.5 seconds is worth it, because as the repo grows, the tradeoff will
// become more worth it.
progressEvent.total = connection.getContentLength();
Log.d("FDroid", "Downloading " + progressEvent.total + " bytes from " + url);
InputStream input = null;
OutputStream output = null;
try {
input = connection.getInputStream();
output = ctx.openFileOutput(dest, Context.MODE_PRIVATE);
Utils.copy(input, output, progressListener, progressEvent);
} finally {
Utils.closeQuietly(output);
Utils.closeQuietly(input);
}
String et = connection.getHeaderField("ETag");
if (et != null)
retag.append(et);
}
Log.d("FDroid", "Fetched " + url + " (" + progressEvent.total +
" bytes) in " + (System.currentTimeMillis() - startTime) +
"ms");
return code;
}
// Do an update from the given repo. All applications found, and their
// APKs, are added to 'apps'. (If 'apps' already contains an app, its
// APKs are merged into the existing one).
// Returns null if successful, otherwise an error message to be displayed
// to the user (if there is an interactive user!)
// 'newetag' should be passed empty. On success, it may contain an etag
// value for the index that was successfully processed, or it may contain
// null if none was available.
public static String doUpdate(Context ctx, DB.Repo repo,
List<DB.App> appsList, StringBuilder newetag, List<Integer> keeprepos,
ProgressListener progressListener) {
try {
int code = 0;
if (repo.pubkey != null) {
// This is a signed repo - we download the jar file,
// check the signature, and extract the index...
Log.d("FDroid", "Getting signed index from " + repo.address + " at " +
logDateFormat.format(new Date(System.currentTimeMillis())));
String address = repo.address + "/index.jar?"
+ ctx.getString(R.string.version_name);
Bundle progressData = createProgressData(repo.address);
ProgressListener.Event event = new ProgressListener.Event(
RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD, progressData);
code = getRemoteFile(ctx, address, "tempindex.jar",
repo.lastetag, newetag, progressListener, event );
if (code == 200) {
String jarpath = ctx.getFilesDir() + "/tempindex.jar";
JarFile jar = null;
JarEntry je;
Certificate[] certs;
try {
jar = new JarFile(jarpath, true);
je = (JarEntry) jar.getEntry("index.xml");
File efile = new File(ctx.getFilesDir(),
"/tempindex.xml");
InputStream input = null;
OutputStream output = null;
try {
input = jar.getInputStream(je);
output = new FileOutputStream(efile);
Utils.copy(input, output);
} finally {
Utils.closeQuietly(output);
Utils.closeQuietly(input);
}
certs = je.getCertificates();
} catch (SecurityException e) {
Log.e("FDroid", "Invalid hash for index file");
return "Invalid hash for index file";
} finally {
if (jar != null) {
jar.close();
}
}
if (certs == null) {
Log.d("FDroid", "No signature found in index");
return "No signature found in index";
}
Log.d("FDroid", "Index has " + certs.length + " signature"
+ (certs.length > 1 ? "s." : "."));
boolean match = false;
for (Certificate cert : certs) {
String certdata = Hasher.hex(cert.getEncoded());
if (repo.pubkey.equals(certdata)) {
match = true;
break;
}
}
if (!match) {
Log.d("FDroid", "Index signature mismatch");
return "Index signature mismatch";
}
}
} else {
// It's an old-fashioned unsigned repo...
Log.d("FDroid", "Getting unsigned index from " + repo.address);
Bundle eventData = createProgressData(repo.address);
ProgressListener.Event event = new ProgressListener.Event(
RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD, eventData);
code = getRemoteFile(ctx, repo.address + "/index.xml",
"tempindex.xml", repo.lastetag, newetag,
progressListener, event);
}
if (code == 200) {
// Process the index...
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser sp = spf.newSAXParser();
XMLReader xr = sp.getXMLReader();
RepoXMLHandler handler = new RepoXMLHandler(repo, appsList, progressListener);
xr.setContentHandler(handler);
File tempIndex = new File(ctx.getFilesDir() + "/tempindex.xml");
BufferedReader r = new BufferedReader(new FileReader(tempIndex));
// A bit of a hack, this might return false positives if an apps description
// or some other part of the XML file contains this, but it is a pretty good
// estimate and makes the progress counter more informative.
// As with asking the server about the size of the index before downloading,
// this also has a time tradeoff. It takes about three seconds to iterate
// through the file and count 600 apps on a slow emulator (v17), but if it is
// taking two minutes to update, the three second wait may be worth it.
final String APPLICATION = "<application";
handler.setTotalAppCount(Utils.countSubstringOccurrence(tempIndex, APPLICATION));
InputSource is = new InputSource(r);
xr.parse(is);
if (handler.pubkey != null && repo.pubkey == null) {
// We read an unsigned index, but that indicates that
// a signed version is now available...
Log.d("FDroid",
"Public key found - switching to signed repo for future updates");
repo.pubkey = handler.pubkey;
try {
DB db = DB.getDB();
db.updateRepoByAddress(repo);
} finally {
DB.releaseDB();
}
}
boolean updateRepo = false;
if (handler.version != null) {
int version = Integer.parseInt(handler.version);
if (version != repo.version) {
Log.d("FDroid", "Repo specified a new version: from "
+ repo.version + " to " + version);
repo.version = version;
updateRepo = true;
}
}
if (handler.maxage != null) {
int maxage = Integer.parseInt(handler.maxage);
if (maxage != repo.maxage) {
Log.d("FDroid",
"Repo specified a new maximum age - updated");
repo.maxage = maxage;
updateRepo = true;
}
}
if (updateRepo) {
try {
DB db = DB.getDB();
db.updateRepoByAddress(repo);
} finally {
DB.releaseDB();
}
}
} else if (code == 304) {
// The index is unchanged since we last read it. We just mark
// everything that came from this repo as being updated.
Log.d("FDroid", "Repo index for " + repo.address
+ " is up to date (by etag)");
keeprepos.add(repo.id);
// Make sure we give back the same etag. (The 200 route will
// have supplied a new one.
newetag.append(repo.lastetag);
} else {
return "Failed to read index - HTTP response "
+ Integer.toString(code);
}
} catch (SSLHandshakeException sslex) {
Log.e("FDroid", "SSLHandShakeException updating from "
+ repo.address + ":\n" + Log.getStackTraceString(sslex));
return "A problem occurred while establishing an SSL connection. If this problem persists, AND you have a very old device, you could try using http instead of https for the repo URL.";
} catch (Exception e) {
Log.e("FDroid", "Exception updating from " + repo.address + ":\n"
+ Log.getStackTraceString(e));
return "Failed to update - " + e.getMessage();
} finally {
ctx.deleteFile("tempindex.xml");
ctx.deleteFile("tempindex.jar");
}
return null;
}
>>>>>>> master
public void setTotalAppCount(int totalAppCount) {
this.totalAppCount = totalAppCount;
}

View File

@ -21,34 +21,31 @@ package org.fdroid.fdroid;
import java.util.ArrayList;
import java.util.List;
import android.app.AlarmManager;
import android.app.IntentService;
import android.app.PendingIntent;
import android.app.NotificationManager;
import android.app.*;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.os.*;
import android.preference.PreferenceManager;
import android.util.Log;
import org.fdroid.fdroid.updater.RepoUpdater;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.widget.Toast;
public class UpdateService extends IntentService implements ProgressListener {
public static final String RESULT_MESSAGE = "msg";
public static final int STATUS_CHANGES = 0;
public static final int STATUS_SAME = 1;
public static final int STATUS_ERROR = 2;
public static final int STATUS_INFO = 3;
public static final String RESULT_EVENT = "event";
public static final int STATUS_COMPLETE_WITH_CHANGES = 0;
public static final int STATUS_COMPLETE_AND_SAME = 1;
public static final int STATUS_ERROR = 2;
public static final int STATUS_INFO = 3;
private ResultReceiver receiver = null;
@ -56,6 +53,76 @@ public class UpdateService extends IntentService implements ProgressListener {
super("UpdateService");
}
// For receiving results from the UpdateService when we've told it to
// update in response to a user request.
public static class UpdateReceiver extends ResultReceiver {
private Context context;
private ProgressDialog dialog;
private ProgressListener listener;
public UpdateReceiver(Handler handler) {
super(handler);
}
public UpdateReceiver setContext(Context context) {
this.context = context;
return this;
}
public UpdateReceiver setDialog(ProgressDialog dialog) {
this.dialog = dialog;
return this;
}
public UpdateReceiver setListener(ProgressListener listener) {
this.listener = listener;
return this;
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
String message = resultData.getString(UpdateService.RESULT_MESSAGE);
boolean finished = false;
if (resultCode == UpdateService.STATUS_ERROR) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
finished = true;
} else if (resultCode == UpdateService.STATUS_COMPLETE_WITH_CHANGES
|| resultCode == UpdateService.STATUS_COMPLETE_AND_SAME) {
finished = true;
} else if (resultCode == UpdateService.STATUS_INFO) {
dialog.setMessage(message);
}
// Forward the progress event on to anybody else who'd like to know.
if (listener != null) {
Parcelable event = resultData.getParcelable(UpdateService.RESULT_EVENT);
if (event != null && event instanceof Event) {
listener.onProgress((Event)event);
}
}
if (finished && dialog.isShowing())
dialog.dismiss();
}
}
public static UpdateReceiver updateNow(Context context) {
String title = context.getString(R.string.process_wait_title);
String message = context.getString(R.string.process_update_msg);
ProgressDialog dialog = ProgressDialog.show(context, title, message, true, true);
dialog.setIcon(android.R.drawable.ic_dialog_info);
dialog.setCanceledOnTouchOutside(false);
Intent intent = new Intent(context, UpdateService.class);
UpdateReceiver receiver = new UpdateReceiver(new Handler());
receiver.setContext(context).setDialog(dialog);
intent.putExtra("receiver", receiver);
context.startService(intent);
return receiver;
}
// Schedule (or cancel schedule for) this service, according to the
// current preferences. Should be called a) at boot, b) if the preference
// is changed, or c) on startup, in case we get upgraded.
@ -88,10 +155,17 @@ public class UpdateService extends IntentService implements ProgressListener {
}
protected void sendStatus(int statusCode, String message) {
sendStatus(statusCode, message, null);
}
protected void sendStatus(int statusCode, String message, Event event) {
if (receiver != null) {
Bundle resultData = new Bundle();
if (message != null && message.length() > 0)
resultData.putString(RESULT_MESSAGE, message);
if (event == null)
event = new Event(statusCode);
resultData.putParcelable(RESULT_EVENT, event);
receiver.send(statusCode, resultData);
}
}
@ -202,7 +276,8 @@ public class UpdateService extends IntentService implements ProgressListener {
if (!changes && success) {
Log.d("FDroid",
"Not checking app details or compatibility, because all repos were up to date.");
"Not checking app details or compatibility, " +
"because all repos were up to date.");
} else if (changes && success) {
sendStatus(STATUS_INFO,
@ -313,9 +388,9 @@ public class UpdateService extends IntentService implements ProgressListener {
e.putLong("lastUpdateCheck", System.currentTimeMillis());
e.commit();
if (changes) {
sendStatus(STATUS_CHANGES);
sendStatus(STATUS_COMPLETE_WITH_CHANGES);
} else {
sendStatus(STATUS_SAME);
sendStatus(STATUS_COMPLETE_AND_SAME);
}
}

View File

@ -18,6 +18,9 @@
package org.fdroid.fdroid;
import android.os.Build;
import android.util.Log;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
@ -26,6 +29,8 @@ import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.security.MessageDigest;
import java.util.Formatter;
import android.content.Context;
@ -151,6 +156,36 @@ public final class Utils {
return count;
}
public static String formatFingerprint(DB.Repo repo) {
return formatFingerprint(repo.pubkey);
}
public static String formatFingerprint(String key) {
String fingerprintString;
if (key == null) {
return "";
}
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(Hasher.unhex(key));
byte[] fingerprint = digest.digest();
Formatter formatter = new Formatter(new StringBuilder());
formatter.format("%02X", fingerprint[0]);
for (int i = 1; i < fingerprint.length; i++) {
formatter.format(i % 5 == 0 ? " %02X" : ":%02X",
fingerprint[i]);
}
fingerprintString = formatter.toString();
formatter.close();
} catch (Exception e) {
Log.w("FDroid", "Unable to get certificate fingerprint.\n"
+ Log.getStackTraceString(e));
fingerprintString = "";
}
return fingerprintString;
}
public static File getApkCacheDir(Context context) {
File apkCacheDir = new File(
StorageUtils.getCacheDirectory(context, true), "apks");

View File

@ -0,0 +1,54 @@
package org.fdroid.fdroid.compat;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build;
import android.widget.CompoundButton;
import android.widget.Switch;
import android.widget.ToggleButton;
import org.fdroid.fdroid.ManageRepo;
public abstract class ClipboardCompat {
public abstract String getText();
public static ClipboardCompat create(Context context) {
if (Build.VERSION.SDK_INT >= 11) {
return new HoneycombClipboard(context);
} else {
return new OldClipboard();
}
}
}
class HoneycombClipboard extends ClipboardCompat {
private final ClipboardManager manager;
protected HoneycombClipboard(Context context) {
this.manager =
(ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
}
@Override
public String getText() {
CharSequence text = null;
if (manager.hasPrimaryClip()) {
ClipData data = manager.getPrimaryClip();
if (data.getItemCount() > 0) {
text = data.getItemAt(0).getText();
}
}
return text != null ? text.toString() : null;
}
}
class OldClipboard extends ClipboardCompat {
@Override
public String getText() {
return null;
}
}

View File

@ -0,0 +1,51 @@
package org.fdroid.fdroid.compat;
import android.os.Build;
import android.widget.CompoundButton;
import android.widget.Switch;
import android.widget.ToggleButton;
import org.fdroid.fdroid.ManageRepo;
public abstract class SwitchCompat {
protected final ManageRepo activity;
protected SwitchCompat(ManageRepo activity) {
this.activity = activity;
}
public abstract CompoundButton createSwitch();
public static SwitchCompat create(ManageRepo activity) {
if (Build.VERSION.SDK_INT >= 11) {
return new HoneycombSwitch(activity);
} else {
return new OldSwitch(activity);
}
}
}
class HoneycombSwitch extends SwitchCompat {
protected HoneycombSwitch(ManageRepo activity) {
super(activity);
}
@Override
public CompoundButton createSwitch() {
return new Switch(activity);
}
}
class OldSwitch extends SwitchCompat {
protected OldSwitch(ManageRepo activity) {
super(activity);
}
@Override
public CompoundButton createSwitch() {
return new ToggleButton(activity);
}
}

View File

@ -184,7 +184,7 @@ abstract public class RepoUpdater {
new BufferedReader(new FileReader(indexFile)));
reader.parse(is);
updateRepo(handler.getPubKey(), handler.getMaxAge());
updateRepo(handler);
}
} catch (SAXException e) {
throw new UpdateException(
@ -210,29 +210,49 @@ abstract public class RepoUpdater {
}
}
private void updateRepo(String publicKey, int maxAge) {
boolean changed = false;
private void updateRepo(RepoXMLHandler handler) {
boolean repoChanged = false;
// We read an unsigned index, but that indicates that
// a signed version is now available...
if (publicKey != null && repo.pubkey == null) {
changed = true;
if (handler.getPubKey() != null && repo.pubkey == null) {
// TODO: Spend the time *now* going to get the etag of the signed
// repo, so that we can prevent downloading it next time. Otherwise
// next time we update, we have to download the signed index
// in its entirety, regardless of if it contains the same
// information as the unsigned one does not...
Log.d("FDroid", "Public key found - switching to signed repo " +
"for future updates");
repo.pubkey = publicKey;
Log.d("FDroid",
"Public key found - switching to signed repo for future updates");
repo.pubkey = handler.getPubKey();
repoChanged = true;
}
if (repo.maxage != maxAge) {
changed = true;
repo.maxage = maxAge;
if (handler.getVersion() != -1 && handler.getVersion() != repo.version) {
Log.d("FDroid", "Repo specified a new version: from "
+ repo.version + " to " + handler.getVersion());
repo.version = handler.getVersion();
repoChanged = true;
}
if (handler.getMaxAge() != -1 && handler.getMaxAge() != repo.maxage) {
Log.d("FDroid",
"Repo specified a new maximum age - updated");
repo.maxage = handler.getMaxAge();
repoChanged = true;
}
if (changed) {
if (handler.getDescription() != null && !handler.getDescription().equals(repo.description)) {
repo.description = handler.getDescription();
repoChanged = true;
}
if (handler.getName() != null && !handler.getName().equals(repo.name)) {
repo.name = handler.getName();
repoChanged = true;
}
if (repoChanged) {
try {
DB db = DB.getDB();
db.updateRepoByAddress(repo);

View File

@ -0,0 +1,108 @@
package org.fdroid.fdroid.views;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
import org.fdroid.fdroid.DB;
import org.fdroid.fdroid.ManageRepo;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.compat.SwitchCompat;
import java.util.List;
public class RepoAdapter extends BaseAdapter {
private List<DB.Repo> repositories;
private final ManageRepo activity;
public RepoAdapter(ManageRepo activity) {
this.activity = activity;
refresh();
}
public void refresh() {
try {
DB db = DB.getDB();
repositories = db.getRepos();
} finally {
DB.releaseDB();
}
notifyDataSetChanged();
}
public boolean hasStableIds() {
return true;
}
@Override
public int getCount() {
return repositories.size();
}
@Override
public Object getItem(int position) {
return repositories.get(position);
}
@Override
public long getItemId(int position) {
return getItem(position).hashCode();
}
private static final int SWITCH_ID = 10000;
@Override
public View getView(int position, View view, ViewGroup parent) {
final DB.Repo repository = repositories.get(position);
CompoundButton switchView;
if (view == null) {
view = activity.getLayoutInflater().inflate(R.layout.repo_item,null);
switchView = addSwitchToView(view);
} else {
switchView = (CompoundButton)view.findViewById(SWITCH_ID);
}
switchView.setChecked(repository.inuse);
switchView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
activity.setRepoEnabled(repository, isChecked);
}
});
TextView nameView = (TextView)view.findViewById(R.id.repo_name);
nameView.setText(repository.getName());
RelativeLayout.LayoutParams nameViewLayout =
(RelativeLayout.LayoutParams)nameView.getLayoutParams();
nameViewLayout.addRule(RelativeLayout.LEFT_OF, switchView.getId());
// If we set the signed view to GONE instead of INVISIBLE, then the
// height of each list item varies.
View signedView = view.findViewById(R.id.repo_unsigned);
if (repository.isSigned()) {
signedView.setVisibility(View.INVISIBLE);
} else {
signedView.setVisibility(View.VISIBLE);
}
return view;
}
private CompoundButton addSwitchToView(View parent) {
SwitchCompat switchBuilder = SwitchCompat.create(activity);
CompoundButton switchView = switchBuilder.createSwitch();
switchView.setId(SWITCH_ID);
RelativeLayout.LayoutParams layout = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT
);
layout.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
layout.addRule(RelativeLayout.CENTER_VERTICAL);
switchView.setLayoutParams(layout);
((RelativeLayout)parent).addView(switchView);
return switchView;
}
}

View File

@ -0,0 +1,69 @@
package org.fdroid.fdroid.views;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import org.fdroid.fdroid.DB;
import org.fdroid.fdroid.compat.ActionBarCompat;
import org.fdroid.fdroid.views.fragments.RepoDetailsFragment;
public class RepoDetailsActivity extends FragmentActivity implements RepoDetailsFragment.OnRepoChangeListener {
public static final String ACTION_IS_DELETED = "isDeleted";
public static final String ACTION_IS_ENABLED = "isEnabled";
public static final String ACTION_IS_DISABLED = "isDisabled";
public static final String ACTION_IS_CHANGED = "isChanged";
public static final String DATA_REPO_ID = "repoId";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
RepoDetailsFragment fragment = new RepoDetailsFragment();
fragment.setRepoChangeListener(this);
fragment.setArguments(getIntent().getExtras());
getSupportFragmentManager()
.beginTransaction()
.add(android.R.id.content, fragment)
.commit();
}
ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true);
}
private void finishWithAction(String actionName) {
Intent data = new Intent();
data.putExtra(actionName, true);
data.putExtra(DATA_REPO_ID, getIntent().getIntExtra(RepoDetailsFragment.ARG_REPO_ID, -1));
setResult(RESULT_OK, data);
finish();
}
@Override
public void onDeleteRepo(DB.Repo repo) {
finishWithAction(ACTION_IS_DELETED);
}
@Override
public void onRepoDetailsChanged(DB.Repo repo) {
// Do nothing...
}
@Override
public void onEnableRepo(DB.Repo repo) {
finishWithAction(ACTION_IS_ENABLED);
}
@Override
public void onDisableRepo(DB.Repo repo) {
finishWithAction(ACTION_IS_DISABLED);
}
@Override
public void onUpdatePerformed(DB.Repo repo) {
// do nothing - the actual update is done by the repo fragment...
}
}

View File

@ -0,0 +1,344 @@
package org.fdroid.fdroid.views.fragments;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.view.MenuItemCompat;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.*;
import android.widget.*;
import org.fdroid.fdroid.*;
import java.util.List;
public class RepoDetailsFragment extends Fragment {
public static final String ARG_REPO_ID = "repo_id";
/**
* If the repo has been updated at least once, then we will show
* all of this info, otherwise they will be hidden.
*/
private static final int[] SHOW_IF_EXISTS = {
R.id.label_repo_name,
R.id.text_repo_name,
R.id.label_description,
R.id.text_description,
R.id.label_num_apps,
R.id.text_num_apps,
R.id.label_last_update,
R.id.text_last_update,
R.id.label_signature,
R.id.text_signature,
R.id.text_signature_description
};
/**
* If the repo has <em>not</em> been updated yet, then we only show
* these, otherwise they are hidden.
*/
private static final int[] HIDE_IF_EXISTS = {
R.id.text_not_yet_updated,
R.id.btn_update
};
private static final int DELETE = 0;
private static final int UPDATE = 1;
public void setRepoChangeListener(OnRepoChangeListener listener) {
repoChangeListener = listener;
}
private OnRepoChangeListener repoChangeListener;
public static interface OnRepoChangeListener {
/**
* This fragment is responsible for getting confirmation from the
* user, so you should presume that the user has already consented
* and confirmed to the deletion.
*/
public void onDeleteRepo(DB.Repo repo);
public void onRepoDetailsChanged(DB.Repo repo);
public void onEnableRepo(DB.Repo repo);
public void onDisableRepo(DB.Repo repo);
public void onUpdatePerformed(DB.Repo repo);
}
// TODO: Currently initialised in onCreateView. Not sure if that is the
// best way to go about this...
private DB.Repo repo;
public void onAttach(Activity activity) {
super.onAttach(activity);
}
/**
* After, for example, a repo update, the details will have changed in the
* database. However, or local reference to the DB.Repo object will not
* have been updated. The safest way to deal with this is to reload the
* repo object directly from the database.
*/
private void reloadRepoDetails() {
try {
DB db = DB.getDB();
repo = db.getRepo(repo.id);
} finally {
DB.releaseDB();
}
}
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
int repoId = getArguments().getInt(ARG_REPO_ID);
DB db = DB.getDB();
repo = db.getRepo(repoId);
DB.releaseDB();
if (repo == null) {
Log.e("FDroid", "Error showing details for repo '" + repoId + "'");
return new LinearLayout(container.getContext());
}
ViewGroup repoView = (ViewGroup)inflater.inflate(R.layout.repodetails, null);
updateView(repoView);
// Setup listeners here, rather than in updateView(...),
// because otherwise we will end up adding multiple listeners with
// subsequent calls to updateView().
EditText inputUrl = (EditText)repoView.findViewById(R.id.input_repo_url);
inputUrl.addTextChangedListener(new UrlWatcher());
Button update = (Button)repoView.findViewById(R.id.btn_update);
update.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
performUpdate();
}
});
return repoView;
}
/**
* Populates relevant views with properties from the current repository.
* Decides which views to show and hide depending on the state of the
* repository.
*/
private void updateView(ViewGroup repoView) {
EditText inputUrl = (EditText)repoView.findViewById(R.id.input_repo_url);
inputUrl.setText(repo.address);
if (repo.hasBeenUpdated()) {
updateViewForExistingRepo(repoView);
} else {
updateViewForNewRepo(repoView);
}
}
/**
* Help function to make switching between two view states easier.
* Perhaps there is a better way to do this. I recall that using Adobe
* Flex, there was a thing called "ViewStates" for exactly this. Wonder if
* that exists in Android?
*/
private static void setMultipleViewVisibility(ViewGroup parent,
int[] viewIds,
int visibility) {
for (int viewId : viewIds) {
parent.findViewById(viewId).setVisibility(visibility);
}
}
private void updateViewForNewRepo(ViewGroup repoView) {
setMultipleViewVisibility(repoView, HIDE_IF_EXISTS, View.VISIBLE);
setMultipleViewVisibility(repoView, SHOW_IF_EXISTS, View.GONE);
}
private void updateViewForExistingRepo(ViewGroup repoView) {
setMultipleViewVisibility(repoView, SHOW_IF_EXISTS, View.VISIBLE);
setMultipleViewVisibility(repoView, HIDE_IF_EXISTS, View.GONE);
TextView name = (TextView)repoView.findViewById(R.id.text_repo_name);
TextView numApps = (TextView)repoView.findViewById(R.id.text_num_apps);
TextView lastUpdated = (TextView)repoView.findViewById(R.id.text_last_update);
name.setText(repo.getName());
numApps.setText(Integer.toString(repo.getNumberOfApps()));
setupDescription(repoView, repo);
setupSignature(repoView, repo);
// Repos that existed before this feature was supported will have an
// "Unknown" last update until next time they update...
String lastUpdate = repo.lastUpdated != null
? repo.lastUpdated.toString() : getString(R.string.unknown);
lastUpdated.setText(lastUpdate);
}
private void setupDescription(ViewGroup parent, DB.Repo repo) {
TextView descriptionLabel = (TextView)parent.findViewById(R.id.label_description);
TextView description = (TextView)parent.findViewById(R.id.text_description);
if (repo.description == null || repo.description.length() == 0) {
descriptionLabel.setVisibility(View.GONE);
description.setVisibility(View.GONE);
} else {
descriptionLabel.setVisibility(View.VISIBLE);
description.setVisibility(View.VISIBLE);
}
String descriptionText = repo.description == null
? "" : repo.description.replaceAll("\n", " ");
description.setText(descriptionText);
}
/**
* When an update is performed, notify the listener so that the repo
* list can be updated. We will perform the update ourselves though.
*/
private void performUpdate() {
repo.enable((FDroidApp)getActivity().getApplication());
UpdateService.updateNow(getActivity()).setListener(new ProgressListener() {
@Override
public void onProgress(Event event) {
if (event.type == UpdateService.STATUS_COMPLETE_AND_SAME ||
event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES) {
reloadRepoDetails();
updateView((ViewGroup)getView());
}
}
});
if (repoChangeListener != null) {
repoChangeListener.onUpdatePerformed(repo);
}
}
/**
* When the URL is changed, notify the repoChangeListener.
*/
class UrlWatcher implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void afterTextChanged(Editable s) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (!repo.address.equals(s.toString())) {
repo.address = s.toString();
try {
DB db = DB.getDB();
db.updateRepo(repo);
} finally {
DB.releaseDB();
}
if (repoChangeListener != null) {
repoChangeListener.onRepoDetailsChanged(repo);
}
}
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
menu.clear();
MenuItem update = menu.add(Menu.NONE, UPDATE, 0, R.string.repo_update);
update.setIcon(R.drawable.ic_menu_refresh);
MenuItemCompat.setShowAsAction(update,
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT );
MenuItem delete = menu.add(Menu.NONE, DELETE, 0, R.string.delete);
delete.setIcon(android.R.drawable.ic_menu_delete);
MenuItemCompat.setShowAsAction(delete,
MenuItemCompat.SHOW_AS_ACTION_IF_ROOM |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == DELETE) {
promptForDelete();
return true;
} else if (item.getItemId() == UPDATE) {
performUpdate();
return true;
}
return false;
}
private void promptForDelete() {
new AlertDialog.Builder(getActivity())
.setTitle(R.string.repo_confirm_delete_title)
.setIcon(android.R.drawable.ic_menu_delete)
.setMessage(R.string.repo_confirm_delete_body)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (repoChangeListener != null) {
DB.Repo repo = RepoDetailsFragment.this.repo;
repoChangeListener.onDeleteRepo(repo);
}
}
}).setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Do nothing...
}
}
).show();
}
private void setupSignature(ViewGroup parent, DB.Repo repo) {
TextView signatureView = (TextView)parent.findViewById(R.id.text_signature);
TextView signatureDescView = (TextView)parent.findViewById(R.id.text_signature_description);
String signature;
int signatureColour;
if (repo.pubkey != null && repo.pubkey.length() > 0) {
signature = Utils.formatFingerprint(repo.pubkey);
signatureColour = getResources().getColor(R.color.signed);
signatureDescView.setVisibility(View.GONE);
} else {
signature = getResources().getString(R.string.unsigned);
signatureColour = getResources().getColor(R.color.unsigned);
signatureDescView.setVisibility(View.VISIBLE);
signatureDescView.setText(getResources().getString(R.string.unsigned_description));
}
signatureView.setText(signature);
signatureView.setTextColor(signatureColour);
}
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
}

View File

@ -0,0 +1,8 @@
/*___Generated_by_IDEA___*/
/** Automatically generated file. DO NOT MODIFY */
package org.fdroid.fdroid.tests;
public final class BuildConfig {
public final static boolean DEBUG = true;
}

View File

@ -0,0 +1,7 @@
/*___Generated_by_IDEA___*/
package org.fdroid.fdroid.tests;
/* This stub is for using by IDE only. It is NOT the Manifest class actually packed into APK */
public final class Manifest {
}

View File

@ -0,0 +1,7 @@
/*___Generated_by_IDEA___*/
package org.fdroid.fdroid.tests;
/* This stub is for using by IDE only. It is NOT the R class actually packed into APK */
public final class R {
}

10
tests/local.properties Normal file
View File

@ -0,0 +1,10 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
# location of the SDK. This is only used by Ant
# For customization when using a Version Control System, please read the
# header note.
sdk.dir=/opt/android-sdk

14
tests/project.properties Normal file
View File

@ -0,0 +1,14 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-4