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:
commit
d80f2d58ad
@ -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"
|
||||
|
2
extern/Universal-Image-Loader
vendored
2
extern/Universal-Image-Loader
vendored
@ -1 +1 @@
|
||||
Subproject commit 75ea560049c9a256ca4fba0a70de1971aa852612
|
||||
Subproject commit 66042fe4a38d5e96030144546290ba0404d24e28
|
@ -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
38
res/layout/repo_item.xml
Normal 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
131
res/layout/repodetails.xml
Normal 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>
|
@ -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>
|
||||
<!--
|
||||
|
15
res/menu/manage_repo_context.xml
Normal file
15
res/menu/manage_repo_context.xml
Normal 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
5
res/values/colors.xml
Normal 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
6
res/values/dimens.xml
Normal 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>
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
|
54
src/org/fdroid/fdroid/compat/ClipboardCompat.java
Normal file
54
src/org/fdroid/fdroid/compat/ClipboardCompat.java
Normal 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;
|
||||
}
|
||||
}
|
51
src/org/fdroid/fdroid/compat/SwitchCompat.java
Normal file
51
src/org/fdroid/fdroid/compat/SwitchCompat.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
108
src/org/fdroid/fdroid/views/RepoAdapter.java
Normal file
108
src/org/fdroid/fdroid/views/RepoAdapter.java
Normal 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;
|
||||
}
|
||||
}
|
69
src/org/fdroid/fdroid/views/RepoDetailsActivity.java
Normal file
69
src/org/fdroid/fdroid/views/RepoDetailsActivity.java
Normal 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...
|
||||
}
|
||||
|
||||
}
|
344
src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java
Normal file
344
src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
8
tests/gen/org/fdroid/fdroid/tests/BuildConfig.java
Normal file
8
tests/gen/org/fdroid/fdroid/tests/BuildConfig.java
Normal 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;
|
||||
}
|
7
tests/gen/org/fdroid/fdroid/tests/Manifest.java
Normal file
7
tests/gen/org/fdroid/fdroid/tests/Manifest.java
Normal 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 {
|
||||
}
|
7
tests/gen/org/fdroid/fdroid/tests/R.java
Normal file
7
tests/gen/org/fdroid/fdroid/tests/R.java
Normal 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
10
tests/local.properties
Normal 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
14
tests/project.properties
Normal 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
|
Loading…
x
Reference in New Issue
Block a user