From 254327f9a7700c8196e61f53801f1f12ac825806 Mon Sep 17 00:00:00 2001 From: Daniel McCarney Date: Wed, 11 Dec 2013 11:46:08 -0500 Subject: [PATCH 001/282] Adding support for SPKI pins, trust-on-first-use of TLS certs. In order to support F-droid repositories hosted with HTTPS using a self-signed certificate the f-droid client should prompt the user to trust or 'memorize' the certificate presented by a repository. The MemorizingTrustManager[0] project enables easy integration of a prompting activity and corresponding trust manager implementation. This behaviour is useful to projects such as Kerplapp[1] that boostrap an F-droid repository on a user's device where it isn't possible to acquire a long lived CA vetted TLS certificate. In addition to Trust-on-First-Use (TOFU) behaviour, this patch integrates the PinningTrustManager [2] project by Moxie Marlinspike to allow the FDroid client to ship a hardcoded set of Subject Public Key Identifier pins [3] for the official FDroid repository TLS certificate, and the Guardian Project TLS certificate. Additional pins can be added to the FDroidPins.java class. The upstream release of AndroidPinning by moxie0 uses a minsdk value of 8. The Fdroid client has a minsdk of 5, presenting compatibility issues using the AndroidPinning lib as a submodule. Fortunately it seems there is no technical reason preventing using a minSDK of 5 with AndroidPinning. I have created a fork with this change and submitted a pull req upstream. Until this pull is merged we can use my fork of AndroidPinning as the submodule. The new 'flow' for deciding if a repositories presented TLS certificate should be trusted is as follows: 1) If the certificate was previously trusted by a TOFU action, then the certificate is accepted as trusted 2) If the certificate wasn't previously trusted by a TOFU action but there is a matching SPKI pin then the certificate is accepted as trusted 3) If the certificate wasn't previously trusted by a TOFU action and there is no SPKI pin but the certificate is signed by a trusted Certificate Authority it is accepted as trusted (This is the behaviour of the FDroid client prior to this patch with all other conditions being a hard-fail). 4) If the certificate wasn't previously trusted by a TOFU action and there is no SPKI pin and the certificate is not signed by a trusted CA (i.e. self signed or otherwise) then the user is prompted to TOFU the certificate. The user may choose to trust the certificate for the current connection or forever. If the user chooses an option other than "deny" the certificate is accepted as trusted for the specified duration. Users currently using a TLS protected repository will see *no difference* in user experience after this patch is merged as the only TLS protected repositories that would function prior to this patch were providing certificates that match condition #3. [0] https://github.com/ge0rg/MemorizingTrustManager/wiki/Integration [1] https://github.com/guardianproject/kerplapp [2] https://github.com/moxie0/AndroidPinning [3] https://www.imperialviolet.org/2011/05/04/pinning.html --- .gitmodules | 6 +++ AndroidManifest.xml | 3 ++ README.md | 6 ++- extern/AndroidPinning | 1 + extern/MemorizingTrustManager | 1 + project.properties | 2 + src/org/fdroid/fdroid/FDroidApp.java | 65 +++++++++++++++++++++-- src/org/fdroid/fdroid/FDroidCertPins.java | 56 +++++++++++++++++++ 8 files changed, 134 insertions(+), 6 deletions(-) create mode 160000 extern/AndroidPinning create mode 160000 extern/MemorizingTrustManager create mode 100644 src/org/fdroid/fdroid/FDroidCertPins.java diff --git a/.gitmodules b/.gitmodules index ca1aa6458..eeaf305bc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,9 @@ path = extern/Universal-Image-Loader url = https://github.com/nostra13/Android-Universal-Image-Loader ignore = dirty +[submodule "extern/MemorizingTrustManager"] + path = extern/MemorizingTrustManager + url = https://github.com/ge0rg/MemorizingTrustManager.git +[submodule "extern/AndroidPinning"] + path = extern/AndroidPinning + url = https://github.com/binaryparadox/AndroidPinning.git diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e6366d4ca..0118acdf9 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -200,6 +200,9 @@ android:name="android.app.searchable" android:resource="@xml/searchable" /> + + + diff --git a/README.md b/README.md index 718c54b73..cf4b657a3 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,10 @@ The only required tools are the Android SDK and Apache Ant. ``` git submodule update --init -android update project -p . -android update project -p extern/Universal-Image-Loader/library +android update project -p . --name F-droid +android update lib-project -p extern/Universal-Image-Loader/library +android update lib-project -p extern/AndroidPinning +android update lib-project -p extern/MemorizingTrustManager ant clean release ``` diff --git a/extern/AndroidPinning b/extern/AndroidPinning new file mode 160000 index 000000000..526654e1b --- /dev/null +++ b/extern/AndroidPinning @@ -0,0 +1 @@ +Subproject commit 526654e1b9997b32e513d58d9094d4c1102a6cb3 diff --git a/extern/MemorizingTrustManager b/extern/MemorizingTrustManager new file mode 160000 index 000000000..49452f67a --- /dev/null +++ b/extern/MemorizingTrustManager @@ -0,0 +1 @@ +Subproject commit 49452f67a760dfef77ddaa7e0b7d88c713c4a195 diff --git a/project.properties b/project.properties index 9d03d0bbe..6da907dd2 100644 --- a/project.properties +++ b/project.properties @@ -3,3 +3,5 @@ proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project. target=android-19 android.library.reference.1=extern/Universal-Image-Loader/library +android.library.reference.2=extern/MemorizingTrustManager +android.library.reference.3=extern/AndroidPinning diff --git a/src/org/fdroid/fdroid/FDroidApp.java b/src/org/fdroid/fdroid/FDroidApp.java index 7523c5245..0558d1f25 100644 --- a/src/org/fdroid/fdroid/FDroidApp.java +++ b/src/org/fdroid/fdroid/FDroidApp.java @@ -19,26 +19,45 @@ package org.fdroid.fdroid; import java.io.File; -import java.lang.Runtime; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Semaphore; -import android.app.Application; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + import android.app.Activity; -import android.preference.PreferenceManager; -import android.util.Log; +import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import org.fdroid.fdroid.Utils; +import android.graphics.Bitmap; +import android.preference.PreferenceManager; +import android.util.Log; + import com.nostra13.universalimageloader.cache.disc.impl.LimitedAgeDiscCache; +import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiscCache; import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator; +import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; +import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; import com.nostra13.universalimageloader.utils.StorageUtils; +import de.duenndns.ssl.MemorizingTrustManager; + +import org.thoughtcrime.ssl.pinning.PinningTrustManager; +import org.thoughtcrime.ssl.pinning.SystemKeyStore; + public class FDroidApp extends Application { private static enum Theme { @@ -117,6 +136,44 @@ public class FDroidApp extends Application { .threadPoolSize(Runtime.getRuntime().availableProcessors() * 2) .build(); ImageLoader.getInstance().init(config); + + try { + SSLContext sc = SSLContext.getInstance("TLS"); + X509TrustManager defaultTrustManager = null; + + /* + * init a trust manager factory with a null keystore to access the system trust managers + */ + TrustManagerFactory tmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + KeyStore ks = null; + tmf.init(ks); + TrustManager[] mgrs = tmf.getTrustManagers(); + + if(mgrs.length > 0 && mgrs[0] instanceof X509TrustManager) + defaultTrustManager = (X509TrustManager) mgrs[0]; + + /* + * compose a chain of trust managers as follows: + * MemorizingTrustManager -> Pinning Trust Manager -> System Trust Manager + */ + PinningTrustManager pinMgr = new PinningTrustManager(SystemKeyStore.getInstance(ctx),FDroidCertPins.getPinList(), 0); + MemorizingTrustManager memMgr = new MemorizingTrustManager(ctx, pinMgr, defaultTrustManager); + + /* + * initialize a SSLContext with the outermost trust manager, use this + * context to set the default SSL socket factory for the HTTPSURLConnection + * class. + */ + sc.init(null, new TrustManager[] {memMgr}, new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + } catch (KeyManagementException e) { + Log.e("FDroid", "Unable to set up trust manager chain. KeyManagementException"); + } catch (NoSuchAlgorithmException e) { + Log.e("FDroid", "Unable to set up trust manager chain. NoSuchAlgorithmException"); + } catch (KeyStoreException e) { + Log.e("FDroid", "Unable to set up trust manager chain. KeyStoreException"); + } } private Context ctx; diff --git a/src/org/fdroid/fdroid/FDroidCertPins.java b/src/org/fdroid/fdroid/FDroidCertPins.java new file mode 100644 index 000000000..09627a0fc --- /dev/null +++ b/src/org/fdroid/fdroid/FDroidCertPins.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.fdroid.fdroid; + +import java.util.ArrayList; +import java.util.Arrays; + +public class FDroidCertPins { + public static final String[] DEFAULT_PINS = + { + /* + * SubjectDN: CN=f-droid.org, OU=PositiveSSL, OU=Domain Control Validated + * IssuerDN: CN=PositiveSSL CA 2, O=COMODO CA Limited, L=Salford, ST=Greater Manchester, C=GB + * Fingerprint: 84B91CDF2312CB9BA7F3BE803783302F8D8C299F + * SPKI Pin: 638F93856E1F5EDFCBD40C46D4160CFF21B0713A + */ + "638F93856E1F5EDFCBD40C46D4160CFF21B0713A", + + /* + * SubjectDN: CN=guardianproject.info, OU=Gandi Standard SSL, OU=Domain Control Validated + * IssuerDN: CN=Gandi Standard SSL CA, O=GANDI SAS, C=FR + * Fingerprint: 187C2573E924DFCBFF2A781A2F99D71C6E031828 + * SPKI Pin: EB6BBC6C6BAEEA20CB0F3357720D86E0F3A526F4 + */ + "EB6BBC6C6BAEEA20CB0F3357720D86E0F3A526F4", + }; + + public static ArrayList PINLIST = null; + + public static String[] getPinList() + { + if(PINLIST == null) + { + PINLIST = new ArrayList(); + PINLIST.addAll(Arrays.asList(DEFAULT_PINS)); + } + + return PINLIST.toArray(new String[PINLIST.size()]); + } +} From 30140d7a3ba0fa2f538d3571a22cfcb2cdcd1708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 8 Jan 2014 23:23:32 +0100 Subject: [PATCH 002/282] Indenting fixes --- src/org/fdroid/fdroid/FDroidApp.java | 14 +++++++------- src/org/fdroid/fdroid/FDroidCertPins.java | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/org/fdroid/fdroid/FDroidApp.java b/src/org/fdroid/fdroid/FDroidApp.java index 0558d1f25..54d48bb1e 100644 --- a/src/org/fdroid/fdroid/FDroidApp.java +++ b/src/org/fdroid/fdroid/FDroidApp.java @@ -139,19 +139,19 @@ public class FDroidApp extends Application { try { SSLContext sc = SSLContext.getInstance("TLS"); - X509TrustManager defaultTrustManager = null; + X509TrustManager defaultTrustManager = null; - /* - * init a trust manager factory with a null keystore to access the system trust managers - */ + /* + * init a trust manager factory with a null keystore to access the system trust managers + */ TrustManagerFactory tmf = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); KeyStore ks = null; tmf.init(ks); TrustManager[] mgrs = tmf.getTrustManagers(); if(mgrs.length > 0 && mgrs[0] instanceof X509TrustManager) - defaultTrustManager = (X509TrustManager) mgrs[0]; + defaultTrustManager = (X509TrustManager) mgrs[0]; /* * compose a chain of trust managers as follows: @@ -173,7 +173,7 @@ public class FDroidApp extends Application { Log.e("FDroid", "Unable to set up trust manager chain. NoSuchAlgorithmException"); } catch (KeyStoreException e) { Log.e("FDroid", "Unable to set up trust manager chain. KeyStoreException"); - } + } } private Context ctx; diff --git a/src/org/fdroid/fdroid/FDroidCertPins.java b/src/org/fdroid/fdroid/FDroidCertPins.java index 09627a0fc..9ecf61847 100644 --- a/src/org/fdroid/fdroid/FDroidCertPins.java +++ b/src/org/fdroid/fdroid/FDroidCertPins.java @@ -24,21 +24,21 @@ import java.util.Arrays; public class FDroidCertPins { public static final String[] DEFAULT_PINS = { - /* - * SubjectDN: CN=f-droid.org, OU=PositiveSSL, OU=Domain Control Validated - * IssuerDN: CN=PositiveSSL CA 2, O=COMODO CA Limited, L=Salford, ST=Greater Manchester, C=GB + /* + * SubjectDN: CN=f-droid.org, OU=PositiveSSL, OU=Domain Control Validated + * IssuerDN: CN=PositiveSSL CA 2, O=COMODO CA Limited, L=Salford, ST=Greater Manchester, C=GB * Fingerprint: 84B91CDF2312CB9BA7F3BE803783302F8D8C299F * SPKI Pin: 638F93856E1F5EDFCBD40C46D4160CFF21B0713A - */ - "638F93856E1F5EDFCBD40C46D4160CFF21B0713A", - - /* - * SubjectDN: CN=guardianproject.info, OU=Gandi Standard SSL, OU=Domain Control Validated + */ + "638F93856E1F5EDFCBD40C46D4160CFF21B0713A", + + /* + * SubjectDN: CN=guardianproject.info, OU=Gandi Standard SSL, OU=Domain Control Validated * IssuerDN: CN=Gandi Standard SSL CA, O=GANDI SAS, C=FR * Fingerprint: 187C2573E924DFCBFF2A781A2F99D71C6E031828 * SPKI Pin: EB6BBC6C6BAEEA20CB0F3357720D86E0F3A526F4 - */ - "EB6BBC6C6BAEEA20CB0F3357720D86E0F3A526F4", + */ + "EB6BBC6C6BAEEA20CB0F3357720D86E0F3A526F4", }; public static ArrayList PINLIST = null; From a666b53aceadbbbc9a91bccb03cf6c9b6f8d3514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 8 Jan 2014 23:44:44 +0100 Subject: [PATCH 003/282] Add ignore=dirty to submodules --- .gitmodules | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index eeaf305bc..c242b4487 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,10 +1,12 @@ [submodule "extern/Universal-Image-Loader"] path = extern/Universal-Image-Loader url = https://github.com/nostra13/Android-Universal-Image-Loader - ignore = dirty + ignore = dirty [submodule "extern/MemorizingTrustManager"] path = extern/MemorizingTrustManager url = https://github.com/ge0rg/MemorizingTrustManager.git + ignore = dirty [submodule "extern/AndroidPinning"] path = extern/AndroidPinning url = https://github.com/binaryparadox/AndroidPinning.git + ignore = dirty From 1c988a0b5a4d91b3b8730e21046c073f7fb73ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 8 Jan 2014 23:54:59 +0100 Subject: [PATCH 004/282] DBHelper refactor fix: re-add version column --- src/org/fdroid/fdroid/data/DBHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index 6bba0054b..272a29551 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -20,7 +20,7 @@ public class DBHelper extends SQLiteOpenHelper { + DB.TABLE_REPO + " (id integer primary key, address text not null, " + "name text, description text, inuse integer not null, " + "priority integer not null, pubkey text, fingerprint text, " - + "maxage integer not null default 0, " + + "maxage integer not null default 0, version integer not null default 0," + "lastetag text, lastUpdated string);"; private static final String CREATE_TABLE_APK = "create table " + DB.TABLE_APK From 2d3c333b21de48392da1cf28f44dadd5403ea49d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 8 Jan 2014 23:55:16 +0100 Subject: [PATCH 005/282] AndroidPinning uses a weird target, force android-17 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cf4b657a3..d07f067b9 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The only required tools are the Android SDK and Apache Ant. git submodule update --init android update project -p . --name F-droid android update lib-project -p extern/Universal-Image-Loader/library -android update lib-project -p extern/AndroidPinning +android update lib-project -p extern/AndroidPinning -t android-17 android update lib-project -p extern/MemorizingTrustManager ant clean release ``` From c0fad0fe26c848458d49b61c843d0dceee0df9a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 9 Jan 2014 12:25:20 +0100 Subject: [PATCH 006/282] Fix: Don't crash if an app has no categories --- src/org/fdroid/fdroid/AppDetails.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index b09f724c1..3ca9cc7a3 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -468,8 +468,10 @@ public class AppDetails extends ListActivity { tv = (TextView) findViewById(R.id.license); tv.setText(app.license); - tv = (TextView) findViewById(R.id.categories); - tv.setText(app.categories.toString().replaceAll(",",", ")); + if (app.categories != null) { + tv = (TextView) findViewById(R.id.categories); + tv.setText(app.categories.toString().replaceAll(",",", ")); + } tv = (TextView) infoView.findViewById(R.id.description); From 9b28fde89f7fa92db4f9943a5b72944b48d25cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 9 Jan 2014 12:35:38 +0100 Subject: [PATCH 007/282] Rename build.prop to ant.prop, add ant-prepare.sh --- README.md | 5 +---- ant-prepare.sh | 6 ++++++ build.properties => ant.properties | 0 3 files changed, 7 insertions(+), 4 deletions(-) create mode 100755 ant-prepare.sh rename build.properties => ant.properties (100%) diff --git a/README.md b/README.md index d07f067b9..35ddb7e05 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,7 @@ The only required tools are the Android SDK and Apache Ant. ``` git submodule update --init -android update project -p . --name F-droid -android update lib-project -p extern/Universal-Image-Loader/library -android update lib-project -p extern/AndroidPinning -t android-17 -android update lib-project -p extern/MemorizingTrustManager +./ant-prepare.sh # This runs 'android update' on the libs and the main project ant clean release ``` diff --git a/ant-prepare.sh b/ant-prepare.sh new file mode 100755 index 000000000..7de7d1df7 --- /dev/null +++ b/ant-prepare.sh @@ -0,0 +1,6 @@ +#!/bin/bash -ex + +android update lib-project -p extern/Universal-Image-Loader/library +android update lib-project -p extern/AndroidPinning -t android-17 +android update lib-project -p extern/MemorizingTrustManager +android update project -p . --name F-Droid diff --git a/build.properties b/ant.properties similarity index 100% rename from build.properties rename to ant.properties From 9f4bfe015c3baa3a38ffcbc81b01387f4438fbf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 10 Jan 2014 18:03:02 +0100 Subject: [PATCH 008/282] Fix: Use PreferencesActivity resultCodes properly This fixes useless restarts/reloads, and missing ones too. --- .../fdroid/fdroid/PreferencesActivity.java | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/org/fdroid/fdroid/PreferencesActivity.java b/src/org/fdroid/fdroid/PreferencesActivity.java index 5491ce26c..109b08870 100644 --- a/src/org/fdroid/fdroid/PreferencesActivity.java +++ b/src/org/fdroid/fdroid/PreferencesActivity.java @@ -85,8 +85,7 @@ public class PreferencesActivity extends PreferenceActivity implements pref.getText())); } - - protected void updateSummary(String key) { + protected void updateSummary(String key, boolean changing) { if (key.equals(Preferences.PREF_UPD_INTERVAL)) { ListPreference pref = (ListPreference)findPreference( @@ -122,20 +121,26 @@ public class PreferencesActivity extends PreferenceActivity implements } else if (key.equals(Preferences.PREF_THEME)) { entrySummary(key); - result |= RESULT_RESTART; - setResult(result); + if (changing) { + result |= RESULT_RESTART; + setResult(result); + } } else if (key.equals(Preferences.PREF_INCOMP_VER)) { onoffSummary(key, R.string.show_incompat_versions_on, R.string.show_incompat_versions_off); - result ^= RESULT_RELOAD; - setResult(result); + if (changing) { + result ^= RESULT_RELOAD; + setResult(result); + } } else if (key.equals(Preferences.PREF_ROOTED)) { onoffSummary(key, R.string.rooted_on, R.string.rooted_off); - result ^= RESULT_REFILTER; - setResult(result); + if (changing) { + result ^= RESULT_REFILTER; + setResult(result); + } } else if (key.equals(Preferences.PREF_IGN_TOUCH)) { onoffSummary(key, R.string.ignoreTouch_on, @@ -163,7 +168,7 @@ public class PreferencesActivity extends PreferenceActivity implements (OnSharedPreferenceChangeListener)this); for (String key : summariesToUpdate) { - updateSummary(key); + updateSummary(key, false); } } @@ -188,7 +193,7 @@ public class PreferencesActivity extends PreferenceActivity implements public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String key) { - updateSummary(key); + updateSummary(key, true); } } From 6a49d9656c95ebe2b5dce841d820e8ee66e42163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 10 Jan 2014 21:02:27 +0100 Subject: [PATCH 009/282] Update changelog --- CHANGELOG.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 760fe9829..5c78474d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ### Upcoming release +* Download icons with a resolution that matches the device's screen density, + which saves resources on smaller devices and gets rid of unnecessary + blurriness on larger devices + * Tweaked some layouts, especially the app lists and their compact layout * App lists now show more useful version information: current version names, @@ -10,13 +14,13 @@ * Slightly increase performance in repo index XML handling by mapping apps with a HashMap, as opposed to doing linear searches -* More info on App Details: The category in which the app was found, all the - categories the app is in and the Android version required to run each one of - its versions available. +* More app info shown in App Details: The category in which the app was found + and all the categories the app is in, as well as the Android version + required to run each one of its versions available * The preferences screen now uses descriptive summaries, which means that you can see what the checkbox preferences actually mean and what the edit and - list preferences are set at. + list preferences are set at * Support for dogecoin donation method added (wow) From 15c1b98d5c0ca678200e41b2d72e90adf93d51df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 10 Jan 2014 21:29:25 +0100 Subject: [PATCH 010/282] Release 0.58 --- AndroidManifest.xml | 4 ++-- CHANGELOG.md | 2 +- res/values/no_trans.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 6f8cb2730..f5e374bde 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="580" + android:versionName="0.58" > F-Droid - 0.57-test + 0.58 https://f-droid.org team@f-droid.org From a5c66a8c6ed9c9fd174aeb448e428d288e52f1e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 10 Jan 2014 22:12:57 +0100 Subject: [PATCH 011/282] New setting: "Small screen" to avoid ellipsizing on small screens --- res/values/strings.xml | 3 +++ res/xml/preferences.xml | 3 +++ src/org/fdroid/fdroid/AppDetails.java | 6 ++++- src/org/fdroid/fdroid/Preferences.java | 26 +++++++++++++++++++ .../fdroid/fdroid/PreferencesActivity.java | 11 ++++---- .../fdroid/fdroid/views/AppListAdapter.java | 16 ++++++++++-- .../views/fragments/AppListFragment.java | 2 ++ 7 files changed, 59 insertions(+), 8 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index f0d7d4506..d7af74136 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -163,6 +163,9 @@ Compact Layout Show icons at a smaller size Show icons at regular size + Small screen + Adapt layouts to smaller screens + Use normal layouts Theme Unsigned URL diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 7b8cbd11e..3d4b3922d 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -25,6 +25,9 @@ + 0) { holder.api.setText(getString(R.string.minsdk_or_later, Utils.getAndroidVersionName(apk.minSdkVersion))); + holder.api.setEnabled(apk.compatible); + holder.api.setVisibility(View.VISIBLE); } else { - holder.api.setText(""); + holder.api.setVisibility(View.GONE); } if (apk.srcname != null) { @@ -325,6 +327,7 @@ public class AppDetails extends ListActivity { SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(getBaseContext()); + pref_smallDensity = prefs.getBoolean("smallDensity", false); pref_expert = prefs.getBoolean("expert", false); pref_permissions = prefs.getBoolean("showPermissions", false); pref_incompatibleVersions = prefs.getBoolean( @@ -334,6 +337,7 @@ public class AppDetails extends ListActivity { } + private boolean pref_smallDensity; private boolean pref_expert; private boolean pref_permissions; private boolean pref_incompatibleVersions; diff --git a/src/org/fdroid/fdroid/Preferences.java b/src/org/fdroid/fdroid/Preferences.java index d90b46de3..052dbe4e6 100644 --- a/src/org/fdroid/fdroid/Preferences.java +++ b/src/org/fdroid/fdroid/Preferences.java @@ -35,17 +35,21 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi public static final String PREF_THEME = "theme"; public static final String PREF_PERMISSIONS = "showPermissions"; public static final String PREF_COMPACT_LAYOUT = "compactlayout"; + public static final String PREF_SMALL_DENSITY = "smallDensity"; public static final String PREF_IGN_TOUCH = "ignoreTouchscreen"; public static final String PREF_CACHE_APK = "cacheDownloaded"; public static final String PREF_EXPERT = "expert"; public static final String PREF_DB_SYNC = "dbSyncMode"; private static final boolean DEFAULT_COMPACT_LAYOUT = false; + private static final boolean DEFAULT_SMALL_DENSITY = false; private boolean compactLayout = DEFAULT_COMPACT_LAYOUT; + private boolean smallDensity = DEFAULT_SMALL_DENSITY; private Map initialized = new HashMap(); private List compactLayoutListeners = new ArrayList(); + private List smallDensityListeners = new ArrayList(); private boolean isInitialized(String key) { return initialized.containsKey(key) && initialized.get(key); @@ -67,6 +71,14 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi return compactLayout; } + public boolean hasSmallDensity() { + if (!isInitialized(PREF_SMALL_DENSITY)) { + initialize(PREF_SMALL_DENSITY); + smallDensity = preferences.getBoolean(PREF_SMALL_DENSITY, DEFAULT_SMALL_DENSITY); + } + return smallDensity; + } + public void registerCompactLayoutChangeListener(ChangeListener listener) { compactLayoutListeners.add(listener); } @@ -75,6 +87,14 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi compactLayoutListeners.remove(listener); } + public void registerSmallDensityChangeListener(ChangeListener listener) { + smallDensityListeners.add(listener); + } + + public void unregisterSmallDensityChangeListener(ChangeListener listener) { + smallDensityListeners.remove(listener); + } + @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Log.d("FDroid", "Invalidating preference '" + key + "'."); @@ -85,6 +105,12 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi listener.onPreferenceChange(); } } + + if (key.equals(PREF_SMALL_DENSITY)) { + for ( ChangeListener listener : smallDensityListeners ) { + listener.onPreferenceChange(); + } + } } private static Preferences instance; diff --git a/src/org/fdroid/fdroid/PreferencesActivity.java b/src/org/fdroid/fdroid/PreferencesActivity.java index 109b08870..48b21edea 100644 --- a/src/org/fdroid/fdroid/PreferencesActivity.java +++ b/src/org/fdroid/fdroid/PreferencesActivity.java @@ -51,6 +51,7 @@ public class PreferencesActivity extends PreferenceActivity implements Preferences.PREF_THEME, Preferences.PREF_PERMISSIONS, Preferences.PREF_COMPACT_LAYOUT, + Preferences.PREF_SMALL_DENSITY, Preferences.PREF_IGN_TOUCH, Preferences.PREF_CACHE_APK, Preferences.PREF_EXPERT, @@ -67,11 +68,7 @@ public class PreferencesActivity extends PreferenceActivity implements protected void onoffSummary(String key, int on, int off) { CheckBoxPreference pref = (CheckBoxPreference)findPreference(key); - if (pref.isChecked()) { - pref.setSummary(on); - } else { - pref.setSummary(off); - } + pref.setSummary(pref.isChecked() ? on : off); } protected void entrySummary(String key) { @@ -119,6 +116,10 @@ public class PreferencesActivity extends PreferenceActivity implements onoffSummary(key, R.string.compactlayout_on, R.string.compactlayout_off); + } else if (key.equals(Preferences.PREF_SMALL_DENSITY)) { + onoffSummary(key, R.string.small_density_on, + R.string.small_density_off); + } else if (key.equals(Preferences.PREF_THEME)) { entrySummary(key); if (changing) { diff --git a/src/org/fdroid/fdroid/views/AppListAdapter.java b/src/org/fdroid/fdroid/views/AppListAdapter.java index d1ad13000..df2157724 100644 --- a/src/org/fdroid/fdroid/views/AppListAdapter.java +++ b/src/org/fdroid/fdroid/views/AppListAdapter.java @@ -91,6 +91,7 @@ abstract public class AppListAdapter extends BaseAdapter { public View getView(int position, View convertView, ViewGroup parent) { boolean compact = Preferences.get().hasCompactLayout(); + boolean small = Preferences.get().hasSmallDensity(); DB.App app = items.get(position); ViewHolder holder; @@ -116,8 +117,19 @@ abstract public class AppListAdapter extends BaseAdapter { ImageLoader.getInstance().displayImage(app.iconUrl, holder.icon, displayImageOptions); - holder.status.setText(getVersionInfo(app)); - holder.license.setText(app.license); + if (small) { + holder.status.setVisibility(View.GONE); + } else { + holder.status.setVisibility(View.VISIBLE); + holder.status.setText(getVersionInfo(app)); + } + + if (small) { + holder.license.setVisibility(View.GONE); + } else { + holder.license.setVisibility(View.VISIBLE); + holder.license.setText(app.license); + } // Disable it all if it isn't compatible... View[] views = { diff --git a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java index 98ee1d9b4..a0095d2c3 100644 --- a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java @@ -26,12 +26,14 @@ abstract class AppListFragment extends Fragment implements AdapterView.OnItemCli public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Preferences.get().registerCompactLayoutChangeListener(this); + Preferences.get().registerSmallDensityChangeListener(this); } @Override public void onDestroy() { super.onDestroy(); Preferences.get().unregisterCompactLayoutChangeListener(this); + Preferences.get().unregisterSmallDensityChangeListener(this); } @Override From 3ae9fd7b88e32a642cfe39a9c9e7b2f7c7c295e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 10 Jan 2014 22:37:26 +0100 Subject: [PATCH 012/282] Try to support AndroidPinning via gradle (won't work as-is) --- build.gradle | 1 + settings.gradle | 1 + 2 files changed, 2 insertions(+) create mode 100644 settings.gradle diff --git a/build.gradle b/build.gradle index a64f6146c..e9e9d7c6d 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ apply plugin: 'android' dependencies { compile files('libs/android-support-v4.jar') + compile project(':extern:AndroidPinning') } android { diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..d6ec4ceb0 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':extern:AndroidPinning' From 3567d9e113b75e102ff8f017342389c229d8cff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 10 Jan 2014 22:41:50 +0100 Subject: [PATCH 013/282] Make fix-ellipsis.sh more accurate * Don't do dirs like res/layout * Do xml files other than strings.xml like arrays.xml --- tools/fix-ellipsis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/fix-ellipsis.sh b/tools/fix-ellipsis.sh index 5a69f89b1..a00e26859 100755 --- a/tools/fix-ellipsis.sh +++ b/tools/fix-ellipsis.sh @@ -2,4 +2,4 @@ # Fix TypographyEllipsis programmatically -find res -name strings.xml -type f | xargs -n 1 sed -i 's/\.\.\./…/g' +find res/values* -name '*.xml' -type f | xargs -n 1 sed -i 's/\.\.\./…/g' From 27452ac31bf9eeba4d97ecce628b19a0eb70157d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 10 Jan 2014 22:52:35 +0100 Subject: [PATCH 014/282] Use View.GONE instead of setText("") --- src/org/fdroid/fdroid/AppDetails.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index f935a8cb9..6ae095538 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -162,7 +162,7 @@ public class AppDetails extends ListActivity { holder.version.setText(getString(R.string.version) + " " + apk.version - + (apk == app.curApk ? " ☆" : "")); + + (apk == app.curApk ? " ☆" : "")); if (apk.vercode == app.installedVerCode && apk.sig.equals(mInstalledSigID)) { @@ -173,14 +173,14 @@ public class AppDetails extends ListActivity { if (apk.detail_size > 0) { holder.size.setText(Utils.getFriendlySize(apk.detail_size)); + holder.size.setVisibility(View.VISIBLE); } else { - holder.size.setText(""); + holder.size.setVisibility(View.GONE); } if (apk.minSdkVersion > 0) { holder.api.setText(getString(R.string.minsdk_or_later, Utils.getAndroidVersionName(apk.minSdkVersion))); - holder.api.setEnabled(apk.compatible); holder.api.setVisibility(View.VISIBLE); } else { holder.api.setVisibility(View.GONE); @@ -195,14 +195,16 @@ public class AppDetails extends ListActivity { if (apk.added != null) { holder.added.setText(getString(R.string.added_on, df.format(apk.added))); + holder.added.setVisibility(View.VISIBLE); } else { - holder.added.setText(""); + holder.added.setVisibility(View.GONE); } if (pref_expert && apk.nativecode != null) { holder.nativecode.setText(apk.nativecode.toString().replaceAll(","," ")); + holder.nativecode.setVisibility(View.VISIBLE); } else { - holder.nativecode.setText(""); + holder.nativecode.setVisibility(View.GONE); } // Disable it all if it isn't compatible... From 99808969d7416e77b540d912e9f35c9f173e6d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 10 Jan 2014 23:32:53 +0100 Subject: [PATCH 015/282] Get rid of remaining UPDATE_REPO stuff from FDroid.java --- src/org/fdroid/fdroid/FDroid.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index d2454bdee..9ef40383e 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -51,11 +51,10 @@ public class FDroid extends FragmentActivity { public static final String EXTRA_TAB_UPDATE = "extraTab"; - private static final int UPDATE_REPO = Menu.FIRST; - private static final int MANAGE_REPO = Menu.FIRST + 1; - private static final int PREFERENCES = Menu.FIRST + 2; - private static final int ABOUT = Menu.FIRST + 3; - private static final int SEARCH = Menu.FIRST + 4; + private static final int MANAGE_REPO = Menu.FIRST; + private static final int PREFERENCES = Menu.FIRST + 1; + private static final int ABOUT = Menu.FIRST + 2; + private static final int SEARCH = Menu.FIRST + 3; private ViewPager viewPager; @@ -144,10 +143,6 @@ public class FDroid extends FragmentActivity { switch (item.getItemId()) { - case UPDATE_REPO: - updateRepos(); - return true; - case MANAGE_REPO: Intent i = new Intent(this, ManageRepo.class); startActivityForResult(i, REQUEST_MANAGEREPOS); From b58cb746121fa84f5bdfa10282b4c9731a34d34f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 14 Jan 2014 13:27:04 +0100 Subject: [PATCH 016/282] Revert "Specify that the version is for Android" This reverts commit aff6a03fa2c2ea84cee6597a44dcfb32fa7006f5. Conflicts: res/values/strings.xml --- res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index d7af74136..60d98f8e1 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -197,6 +197,6 @@ Disabled "%1$s".\n\nYou will need to re-enable this repository to install apps from it. - Android %s or later + %s or later From d8df407b02ca4faefa7f9dfeccb7057906b95994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 15 Jan 2014 16:33:09 +0100 Subject: [PATCH 017/282] Don't hard-code apks.get(0) when showing permissions --- src/org/fdroid/fdroid/AppDetails.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 6ae095538..bf1b07e0e 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -554,10 +554,10 @@ public class AppDetails extends ListActivity { tv = (TextView) infoView.findViewById(R.id.summary); tv.setText(app.summary); - if (pref_permissions && !app.apks.isEmpty()) { + if (pref_permissions && app.curApk != null) { tv = (TextView) infoView.findViewById(R.id.permissions_list); - CommaSeparatedList permsList = app.apks.get(0).detail_permissions; + CommaSeparatedList permsList = app.curApk.detail_permissions; if (permsList == null) { tv.setText(getString(R.string.no_permissions)); } else { From 763b4d3ea061bf85ca474fe3f06ba6279c7d5afd Mon Sep 17 00:00:00 2001 From: Kevin Everets Date: Wed, 15 Jan 2014 15:18:09 -0500 Subject: [PATCH 018/282] Keep track of the reason that an apk is incompatible --- src/org/fdroid/fdroid/DB.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index 19525f405..9a0da7712 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -301,6 +301,8 @@ public class DB { public CommaSeparatedList nativecode; // null if empty or unknown + public CommaSeparatedList incompatible_reasons; // null if empty or + // unknown // ID (md5 sum of public key) of signature. Might be null, in the // transition to this field existing. public String sig; @@ -372,14 +374,17 @@ public class DB { } public boolean isCompatible(Apk apk) { - if (!hasApi(apk.minSdkVersion)) + if (!hasApi(apk.minSdkVersion)) { + apk.incompatible_reasons = CommaSeparatedList.make(String.valueOf(apk.minSdkVersion)); return false; + } if (apk.features != null) { for (String feat : apk.features) { if (ignoreTouchscreen && feat.equals("android.hardware.touchscreen")) { // Don't check it! } else if (!features.contains(feat)) { + apk.incompatible_reasons = CommaSeparatedList.make(feat); Log.d("FDroid", apk.id + " vercode " + apk.vercode + " is incompatible based on lack of " + feat); @@ -388,6 +393,7 @@ public class DB { } } if (!compatibleApi(apk.nativecode)) { + apk.incompatible_reasons = apk.nativecode; Log.d("FDroid", apk.id + " vercode " + apk.vercode + " only supports " + CommaSeparatedList.str(apk.nativecode) + " while your architectures are " + cpuAbisDesc); From 68067a81c914cb13e6f526e10e39492a980920f2 Mon Sep 17 00:00:00 2001 From: Kevin Everets Date: Wed, 15 Jan 2014 16:04:17 -0500 Subject: [PATCH 019/282] Show incompatible reasons if they exist --- src/org/fdroid/fdroid/AppDetails.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index bf1b07e0e..40ef17a90 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -207,6 +207,11 @@ public class AppDetails extends ListActivity { holder.nativecode.setVisibility(View.GONE); } + if (apk.incompatible_reasons != null) { + holder.api.setText(apk.incompatible_reasons.toString()); + holder.api.setVisibility(View.VISIBLE); + } + // Disable it all if it isn't compatible... View[] views = { convertView, From ca1a07677a5d8b684d65a3cab94f754272cf1ad2 Mon Sep 17 00:00:00 2001 From: Kevin Everets Date: Wed, 15 Jan 2014 16:04:48 -0500 Subject: [PATCH 020/282] Move up the setting of Prefences in AppDetails so they are actually respected Without this, no matter how the user set the preferences, they could not see incompatible APKs in the AppDetails. --- src/org/fdroid/fdroid/AppDetails.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 40ef17a90..b8027df6b 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -325,20 +325,20 @@ public class AppDetails extends ListActivity { resetRequired = false; } - // Set up the list... - headerView = new LinearLayout(this); - ListView lv = (ListView) findViewById(android.R.id.list); - lv.addHeaderView(headerView); - ApkListAdapter la = new ApkListAdapter(this, app.apks); - setListAdapter(la); - SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(getBaseContext()); pref_smallDensity = prefs.getBoolean("smallDensity", false); pref_expert = prefs.getBoolean("expert", false); pref_permissions = prefs.getBoolean("showPermissions", false); pref_incompatibleVersions = prefs.getBoolean( - "incompatibleVersions", false); + Preferences.PREF_INCOMP_VER, false); + + // Set up the list... + headerView = new LinearLayout(this); + ListView lv = (ListView) findViewById(android.R.id.list); + lv.addHeaderView(headerView); + ApkListAdapter la = new ApkListAdapter(this, app.apks); + setListAdapter(la); startViews(); From 4a70097c64b2f994ebca13a45edeb3c5adf0acfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 15 Jan 2014 23:44:56 +0100 Subject: [PATCH 021/282] Show version updates in the updates tab --- src/org/fdroid/fdroid/views/CanUpdateAppListAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/views/CanUpdateAppListAdapter.java b/src/org/fdroid/fdroid/views/CanUpdateAppListAdapter.java index efd4416ea..c3dcd91ae 100644 --- a/src/org/fdroid/fdroid/views/CanUpdateAppListAdapter.java +++ b/src/org/fdroid/fdroid/views/CanUpdateAppListAdapter.java @@ -9,7 +9,7 @@ public class CanUpdateAppListAdapter extends AppListAdapter { @Override protected boolean showStatusUpdate() { - return false; + return true; } @Override From a6ed36808d74c6b4603b3b1628cf1ed65cdafaa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 15 Jan 2014 23:45:11 +0100 Subject: [PATCH 022/282] Don't show permissions list if there are no versions shown --- src/org/fdroid/fdroid/AppDetails.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index b8027df6b..70d739a15 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -559,7 +559,8 @@ public class AppDetails extends ListActivity { tv = (TextView) infoView.findViewById(R.id.summary); tv.setText(app.summary); - if (pref_permissions && app.curApk != null) { + if (pref_permissions && app.curApk != null && + (app.curApk.compatible || pref_incompatibleVersions)) { tv = (TextView) infoView.findViewById(R.id.permissions_list); CommaSeparatedList permsList = app.curApk.detail_permissions; From d99e3edb5258c502def2e334c09f46202e5ecba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 15 Jan 2014 23:48:42 +0100 Subject: [PATCH 023/282] Incompatible apks are always in memory, no need to reload --- src/org/fdroid/fdroid/PreferencesActivity.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/org/fdroid/fdroid/PreferencesActivity.java b/src/org/fdroid/fdroid/PreferencesActivity.java index 48b21edea..39fcb7e35 100644 --- a/src/org/fdroid/fdroid/PreferencesActivity.java +++ b/src/org/fdroid/fdroid/PreferencesActivity.java @@ -130,10 +130,6 @@ public class PreferencesActivity extends PreferenceActivity implements } else if (key.equals(Preferences.PREF_INCOMP_VER)) { onoffSummary(key, R.string.show_incompat_versions_on, R.string.show_incompat_versions_off); - if (changing) { - result ^= RESULT_RELOAD; - setResult(result); - } } else if (key.equals(Preferences.PREF_ROOTED)) { onoffSummary(key, R.string.rooted_on, From 305daf5a1063cc7224de15e96a2db23cbf21b9d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 15 Jan 2014 23:51:24 +0100 Subject: [PATCH 024/282] DB.java: remove unused prefs, use pref names from Preferences.java --- src/org/fdroid/fdroid/DB.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index 9a0da7712..2ba046f4b 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -334,7 +334,7 @@ public class DB { SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(ctx); ignoreTouchscreen = prefs - .getBoolean("ignoreTouchscreen", false); + .getBoolean(Preferences.PREF_IGN_TOUCH, false); PackageManager pm = ctx.getPackageManager(); StringBuilder logMsg = new StringBuilder(); @@ -564,7 +564,7 @@ public class DB { db = h.getWritableDatabase(); SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(mContext); - String sync_mode = prefs.getString("dbSyncMode", null); + String sync_mode = prefs.getString(Preferences.PREF_DB_SYNC, null); if ("off".equals(sync_mode)) setSynchronizationMode(SYNC_OFF); else if ("normal".equals(sync_mode)) @@ -784,8 +784,6 @@ public class DB { + (System.currentTimeMillis() - startTime) + " ms)"); List repos = getRepos(); - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(mContext); cols = new String[] { "id", "version", "vercode", "sig", "srcname", "apkName", "minSdkVersion", "added", "features", "nativecode", "compatible", "repo" }; From 9500c987cc8edca7cb766a3ebbee43d3ed11e28f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 16 Jan 2014 09:32:34 +0100 Subject: [PATCH 025/282] Finish getting rid of hard-coded preferences --- src/org/fdroid/fdroid/AppDetails.java | 6 +++--- src/org/fdroid/fdroid/AppFilter.java | 2 +- src/org/fdroid/fdroid/AppListManager.java | 2 +- src/org/fdroid/fdroid/FDroidApp.java | 6 +++--- src/org/fdroid/fdroid/ManageRepo.java | 2 +- src/org/fdroid/fdroid/Preferences.java | 1 + src/org/fdroid/fdroid/UpdateService.java | 13 +++++++------ 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 70d739a15..82058207a 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -327,9 +327,9 @@ public class AppDetails extends ListActivity { SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(getBaseContext()); - pref_smallDensity = prefs.getBoolean("smallDensity", false); - pref_expert = prefs.getBoolean("expert", false); - pref_permissions = prefs.getBoolean("showPermissions", false); + pref_smallDensity = prefs.getBoolean(Preferences.PREF_SMALL_DENSITY, false); + pref_expert = prefs.getBoolean(Preferences.PREF_EXPERT, false); + pref_permissions = prefs.getBoolean(Preferences.PREF_PERMISSIONS, false); pref_incompatibleVersions = prefs.getBoolean( Preferences.PREF_INCOMP_VER, false); diff --git a/src/org/fdroid/fdroid/AppFilter.java b/src/org/fdroid/fdroid/AppFilter.java index 6cd688d7e..9e85f8eae 100644 --- a/src/org/fdroid/fdroid/AppFilter.java +++ b/src/org/fdroid/fdroid/AppFilter.java @@ -31,7 +31,7 @@ public class AppFilter { // Read preferences and cache them so we can do quick lookups. SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(ctx); - pref_rooted = prefs.getBoolean("rooted", true); + pref_rooted = prefs.getBoolean(Preferences.PREF_ROOTED, true); } // Return true if the given app should be filtered out based on user diff --git a/src/org/fdroid/fdroid/AppListManager.java b/src/org/fdroid/fdroid/AppListManager.java index 8e30ee7c1..ee66e5736 100644 --- a/src/org/fdroid/fdroid/AppListManager.java +++ b/src/org/fdroid/fdroid/AppListManager.java @@ -143,7 +143,7 @@ public class AppListManager { private Date calcMaxHistory() { SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(fdroidActivity.getBaseContext()); - String daysPreference = prefs.getString("updateHistoryDays", "14"); + String daysPreference = prefs.getString(Preferences.PREF_UPD_HISTORY, "14"); int maxHistoryDays = Integer.parseInt(daysPreference); Calendar recent = Calendar.getInstance(); recent.add(Calendar.DAY_OF_YEAR, -maxHistoryDays); diff --git a/src/org/fdroid/fdroid/FDroidApp.java b/src/org/fdroid/fdroid/FDroidApp.java index 54d48bb1e..cd4e0179b 100644 --- a/src/org/fdroid/fdroid/FDroidApp.java +++ b/src/org/fdroid/fdroid/FDroidApp.java @@ -68,7 +68,7 @@ public class FDroidApp extends Application { public void reloadTheme() { curTheme = Theme.valueOf(PreferenceManager .getDefaultSharedPreferences(getBaseContext()) - .getString("theme", "dark")); + .getString(Preferences.PREF_THEME, "dark")); } public void applyTheme(Activity activity) { switch (curTheme) { @@ -96,8 +96,8 @@ public class FDroidApp extends Application { // because the install intent says it's finished when it hasn't. SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(getBaseContext()); - curTheme = Theme.valueOf(prefs.getString("theme", "dark")); - if (!prefs.getBoolean("cacheDownloaded", false)) { + curTheme = Theme.valueOf(prefs.getString(Preferences.PREF_THEME, "dark")); + if (!prefs.getBoolean(Preferences.PREF_CACHE_APK, false)) { File local_path = Utils.getApkCacheDir(this); // Things can be null if the SD card is not ready - we'll just diff --git a/src/org/fdroid/fdroid/ManageRepo.java b/src/org/fdroid/fdroid/ManageRepo.java index ab06b5e20..50191ff71 100644 --- a/src/org/fdroid/fdroid/ManageRepo.java +++ b/src/org/fdroid/fdroid/ManageRepo.java @@ -92,7 +92,7 @@ public class ManageRepo extends ListActivity { .getDefaultSharedPreferences(getBaseContext()); TextView tv_lastCheck = (TextView)findViewById(R.id.lastUpdateCheck); - long lastUpdate = prefs.getLong("lastUpdateCheck", 0); + long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0); String s_lastUpdateCheck = ""; if (lastUpdate == 0) { s_lastUpdateCheck = getString(R.string.never); diff --git a/src/org/fdroid/fdroid/Preferences.java b/src/org/fdroid/fdroid/Preferences.java index 052dbe4e6..825e3d526 100644 --- a/src/org/fdroid/fdroid/Preferences.java +++ b/src/org/fdroid/fdroid/Preferences.java @@ -40,6 +40,7 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi public static final String PREF_CACHE_APK = "cacheDownloaded"; public static final String PREF_EXPERT = "expert"; public static final String PREF_DB_SYNC = "dbSyncMode"; + public static final String PREF_UPD_LAST = "lastUpdateCheck"; private static final boolean DEFAULT_COMPACT_LAYOUT = false; private static final boolean DEFAULT_SMALL_DENSITY = false; diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index 6c5f30b92..d81b2fddd 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -42,6 +42,7 @@ public class UpdateService extends IntentService implements ProgressListener { public static final String RESULT_MESSAGE = "msg"; 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; @@ -130,7 +131,7 @@ public class UpdateService extends IntentService implements ProgressListener { SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(ctx); - String sint = prefs.getString("updateInterval", "0"); + String sint = prefs.getString(Preferences.PREF_UPD_INTERVAL, "0"); int interval = Integer.parseInt(sint); Intent intent = new Intent(ctx, UpdateService.class); @@ -203,8 +204,8 @@ public class UpdateService extends IntentService implements ProgressListener { // See if it's time to actually do anything yet... if (isScheduledRun()) { - long lastUpdate = prefs.getLong("lastUpdateCheck", 0); - String sint = prefs.getString("updateInterval", "0"); + long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0); + String sint = prefs.getString(Preferences.PREF_UPD_INTERVAL, "0"); int interval = Integer.parseInt(sint); if (interval == 0) { Log.d("FDroid", "Skipping update - disabled"); @@ -219,7 +220,7 @@ public class UpdateService extends IntentService implements ProgressListener { // If we are to update the repos only on wifi, make sure that // connection is active - if (prefs.getBoolean("updateOnWifiOnly", false)) { + if (prefs.getBoolean(Preferences.PREF_UPD_WIFI_ONLY, false)) { ConnectivityManager conMan = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo.State wifi = conMan.getNetworkInfo(1).getState(); if (wifi != NetworkInfo.State.CONNECTED && @@ -232,7 +233,7 @@ public class UpdateService extends IntentService implements ProgressListener { Log.d("FDroid", "Unscheduled (manually requested) update"); } - boolean notify = prefs.getBoolean("updateNotify", false); + boolean notify = prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, false); // Grab some preliminary information, then we can release the // database while we do all the downloading, etc... @@ -385,7 +386,7 @@ public class UpdateService extends IntentService implements ProgressListener { sendStatus(STATUS_ERROR, errmsg); } else { Editor e = prefs.edit(); - e.putLong("lastUpdateCheck", System.currentTimeMillis()); + e.putLong(Preferences.PREF_UPD_LAST, System.currentTimeMillis()); e.commit(); if (changes) { sendStatus(STATUS_COMPLETE_WITH_CHANGES); From 1f99c00899c8991fad85e005770d43b4e7ccbb57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 17 Jan 2014 21:06:06 +0100 Subject: [PATCH 026/282] Revert "New setting: "Small screen" to avoid ellipsizing on small screens" This reverts commit a5c66a8c6ed9c9fd174aeb448e428d288e52f1e1. Conflicts: src/org/fdroid/fdroid/AppDetails.java --- res/values/strings.xml | 3 --- res/xml/preferences.xml | 3 --- src/org/fdroid/fdroid/AppDetails.java | 2 -- src/org/fdroid/fdroid/Preferences.java | 26 ------------------- .../fdroid/fdroid/PreferencesActivity.java | 11 ++++---- .../fdroid/fdroid/views/AppListAdapter.java | 16 ++---------- .../views/fragments/AppListFragment.java | 2 -- 7 files changed, 7 insertions(+), 56 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 60d98f8e1..537211193 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -163,9 +163,6 @@ Compact Layout Show icons at a smaller size Show icons at regular size - Small screen - Adapt layouts to smaller screens - Use normal layouts Theme Unsigned URL diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 3d4b3922d..7b8cbd11e 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -25,9 +25,6 @@ - initialized = new HashMap(); private List compactLayoutListeners = new ArrayList(); - private List smallDensityListeners = new ArrayList(); private boolean isInitialized(String key) { return initialized.containsKey(key) && initialized.get(key); @@ -72,14 +68,6 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi return compactLayout; } - public boolean hasSmallDensity() { - if (!isInitialized(PREF_SMALL_DENSITY)) { - initialize(PREF_SMALL_DENSITY); - smallDensity = preferences.getBoolean(PREF_SMALL_DENSITY, DEFAULT_SMALL_DENSITY); - } - return smallDensity; - } - public void registerCompactLayoutChangeListener(ChangeListener listener) { compactLayoutListeners.add(listener); } @@ -88,14 +76,6 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi compactLayoutListeners.remove(listener); } - public void registerSmallDensityChangeListener(ChangeListener listener) { - smallDensityListeners.add(listener); - } - - public void unregisterSmallDensityChangeListener(ChangeListener listener) { - smallDensityListeners.remove(listener); - } - @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Log.d("FDroid", "Invalidating preference '" + key + "'."); @@ -106,12 +86,6 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi listener.onPreferenceChange(); } } - - if (key.equals(PREF_SMALL_DENSITY)) { - for ( ChangeListener listener : smallDensityListeners ) { - listener.onPreferenceChange(); - } - } } private static Preferences instance; diff --git a/src/org/fdroid/fdroid/PreferencesActivity.java b/src/org/fdroid/fdroid/PreferencesActivity.java index 39fcb7e35..f66066d76 100644 --- a/src/org/fdroid/fdroid/PreferencesActivity.java +++ b/src/org/fdroid/fdroid/PreferencesActivity.java @@ -51,7 +51,6 @@ public class PreferencesActivity extends PreferenceActivity implements Preferences.PREF_THEME, Preferences.PREF_PERMISSIONS, Preferences.PREF_COMPACT_LAYOUT, - Preferences.PREF_SMALL_DENSITY, Preferences.PREF_IGN_TOUCH, Preferences.PREF_CACHE_APK, Preferences.PREF_EXPERT, @@ -68,7 +67,11 @@ public class PreferencesActivity extends PreferenceActivity implements protected void onoffSummary(String key, int on, int off) { CheckBoxPreference pref = (CheckBoxPreference)findPreference(key); - pref.setSummary(pref.isChecked() ? on : off); + if (pref.isChecked()) { + pref.setSummary(on); + } else { + pref.setSummary(off); + } } protected void entrySummary(String key) { @@ -116,10 +119,6 @@ public class PreferencesActivity extends PreferenceActivity implements onoffSummary(key, R.string.compactlayout_on, R.string.compactlayout_off); - } else if (key.equals(Preferences.PREF_SMALL_DENSITY)) { - onoffSummary(key, R.string.small_density_on, - R.string.small_density_off); - } else if (key.equals(Preferences.PREF_THEME)) { entrySummary(key); if (changing) { diff --git a/src/org/fdroid/fdroid/views/AppListAdapter.java b/src/org/fdroid/fdroid/views/AppListAdapter.java index df2157724..d1ad13000 100644 --- a/src/org/fdroid/fdroid/views/AppListAdapter.java +++ b/src/org/fdroid/fdroid/views/AppListAdapter.java @@ -91,7 +91,6 @@ abstract public class AppListAdapter extends BaseAdapter { public View getView(int position, View convertView, ViewGroup parent) { boolean compact = Preferences.get().hasCompactLayout(); - boolean small = Preferences.get().hasSmallDensity(); DB.App app = items.get(position); ViewHolder holder; @@ -117,19 +116,8 @@ abstract public class AppListAdapter extends BaseAdapter { ImageLoader.getInstance().displayImage(app.iconUrl, holder.icon, displayImageOptions); - if (small) { - holder.status.setVisibility(View.GONE); - } else { - holder.status.setVisibility(View.VISIBLE); - holder.status.setText(getVersionInfo(app)); - } - - if (small) { - holder.license.setVisibility(View.GONE); - } else { - holder.license.setVisibility(View.VISIBLE); - holder.license.setText(app.license); - } + holder.status.setText(getVersionInfo(app)); + holder.license.setText(app.license); // Disable it all if it isn't compatible... View[] views = { diff --git a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java index a0095d2c3..98ee1d9b4 100644 --- a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java @@ -26,14 +26,12 @@ abstract class AppListFragment extends Fragment implements AdapterView.OnItemCli public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Preferences.get().registerCompactLayoutChangeListener(this); - Preferences.get().registerSmallDensityChangeListener(this); } @Override public void onDestroy() { super.onDestroy(); Preferences.get().unregisterCompactLayoutChangeListener(this); - Preferences.get().unregisterSmallDensityChangeListener(this); } @Override From b734b210e33b2dfa86543c289333fd7b67e5b18e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 19 Jan 2014 20:47:39 +0100 Subject: [PATCH 027/282] Add pd0x's entries to changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1176143f7..3b26e833f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +### Upcoming release + +* Support for repositories using self-signed HTTPS certificates through + Trust-on-first-use popup + +* Support for TLS Subject-Public-Key-Identifier pinning + ### 0.58 (2014-01-11) * Download icons with a resolution that matches the device's screen density, From 077548eb72763461d4797402695ad6be651c1575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 19 Jan 2014 20:50:11 +0100 Subject: [PATCH 028/282] Note that automatic building via gradle is not supported --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 35ddb7e05..a40de5851 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ git submodule update --init ant clean release ``` +The project itself supports Gradle, but some of the libraries it uses don't. +Hence it is currently not possible to build F-Droid with Gradle in a clean way +without manual interaction. + Direct download --------------- From fd998b7566c64069150fd8f5be42369efb5d62ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 20 Jan 2014 14:31:53 +0100 Subject: [PATCH 029/282] Avoid possible crashes when checking signatures Might fix https://f-droid.org/forums/topic/nullpointerexception-when-trying-to-update-puzzles --- src/org/fdroid/fdroid/AppDetails.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 45f1afe4b..a63b4bb29 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -165,6 +165,7 @@ public class AppDetails extends ListActivity { + (apk == app.curApk ? " ☆" : "")); if (apk.vercode == app.installedVerCode + && mInstalledSigID != null && apk.sig != null && apk.sig.equals(mInstalledSigID)) { holder.status.setText(getString(R.string.inst)); } else { From 7e87f8b81fbd1bfba2c36bcf87871f9eef6b2a0f Mon Sep 17 00:00:00 2001 From: F-Droid Translatebot Date: Tue, 21 Jan 2014 11:23:43 +0000 Subject: [PATCH 030/282] Translation updates --- res/values-ar/strings.xml | 1 + res/values-bg/strings.xml | 15 +++++++++++++- res/values-ca/strings.xml | 23 +++++++++++++++++++-- res/values-de/strings.xml | 17 ++++++++++++++++ res/values-el/strings.xml | 21 +++++++++++++++++-- res/values-en-rGB/strings.xml | 7 +++++++ res/values-eo/strings.xml | 3 +++ res/values-es/strings.xml | 21 +++++++++++++++++-- res/values-eu/strings.xml | 16 +++++++++++++-- res/values-fa/strings.xml | 21 +++++++++++++++++-- res/values-fi/strings.xml | 17 ++++++++++++++-- res/values-fr/strings.xml | 19 +++++++++++++++++- res/values-gl/strings.xml | 19 +++++++++++++++++- res/values-it/strings.xml | 21 +++++++++++++++++-- res/values-ko/strings.xml | 16 ++++++++++++++- res/values-nb/strings.xml | 21 +++++++++++++++++-- res/values-nl/strings.xml | 21 +++++++++++++++++-- res/values-pl/array.xml | 4 ++-- res/values-pl/strings.xml | 38 ++++++++++++++++++++++++++++++++++- res/values-pt-rBR/strings.xml | 21 +++++++++++++++++-- res/values-ro/strings.xml | 11 ++++++++-- res/values-ru/strings.xml | 18 +++++++++++++++-- res/values-sl/strings.xml | 13 +++++++++++- res/values-sr/strings.xml | 19 ++++++++++++++++-- res/values-sv/strings.xml | 29 +++++++++++++++++++++++++- res/values-tr/strings.xml | 19 +++++++++++++++++- res/values-ug/strings.xml | 17 ++++++++++++++++ res/values-uk/strings.xml | 13 +++++++++++- res/values-zh-rCN/strings.xml | 11 ++++++++++ 29 files changed, 455 insertions(+), 37 deletions(-) create mode 100644 res/values-en-rGB/strings.xml diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml index 69c22fa0e..6dafe8e8c 100644 --- a/res/values-ar/strings.xml +++ b/res/values-ar/strings.xml @@ -3,4 +3,5 @@ عثر على تطبيق واحد يوافق \'%s\': لم يعثر على أي تطبيق يوافق \'%s\' الإصدار + %d إصدار متوفر diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml index bb8ac1dd4..5071c0d5a 100644 --- a/res/values-bg/strings.xml +++ b/res/values-bg/strings.xml @@ -7,13 +7,20 @@ Изглежда този пакет не е съвместим с твоето устойство. Искаш ли да опиташ да го инсталираш въпреки това? Опитваш се да инсталираш по-стара версия на праложението. Това може да го повреди и дори да изтрие данните ти. Искаш ли да го инсталираш въпреки това? Версия + %d налични версии + %d налична версия Кеширай свалените приложения + Пази свалените apk файлове на SD картата Актуализации Други Последно сканиране на хранилищата: %s никога + Автоматично сканиране на хранилищата + Актуализирай списъка на приложенията от хранилищата автоматично Уведомления + Уведомявай ме при нови налични актуализации Актуализирай историята + Дни за показване на нови/актуализирани приложения Резултати от търсенето Детайли за приложението Такова приложение не беше намерено @@ -40,13 +47,14 @@ Отказ Избери хранилище за премахване Актуализирай хранилищата + Инсталирани Налични За актуализация 1 налична актуализация. %d налични актуализации. Актуализации на F-Droid са налични Моля изчакай - Обновявани на списъка с приложения… + Обновявани на списъка с приложения... Взимане на приложението от Адрес на хранилището Списъкът на хранилищата е променен. @@ -72,11 +80,14 @@ Свалянето е отказано Дисплей Експерт + Активирай експертен режим Търсене на приложения Вид на синхронизация на базата данни Съвместимост на приложенията Root достъп + Показвай приложения изискващи root права Игнорирай сензорния екран + Винаги включвай приложения изискващи сензорен екран Всички Какво ново Обновени наскоро @@ -92,6 +103,8 @@ Не се искат разрешения. Разрешения за версия %s Покажи разрешения + Показване на списък с разрешения, които приложението ползва Нямаш инсталирано приложение, което може да изпълни %s Компактно оформление + Показвай само имената и описанията на приложенията в списъка diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index 087d66546..cf4c87e5c 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -7,14 +7,22 @@ Sembla que aquest paquet no és compatible amb el vostre dispositiu. Voleu provar a instaŀlar-lo de totes maneres? Aneu a desactualitzar aquesta aplicació. Això podria fer que l\'aplicació no funcionés o inclús es perdessin les vostres dades. Esteu segur que ho voleu fer? Versió + Hi ha %d versions disponibles + Hi ha %d versió disponible Memòria cau de les aplicacions baixades + Desa els fitxers apk baixats a la targeta SD Actualitzacions Altres Darrera actualització dels dipòsits: %s mai + Actualitza automàticament els dipòsits + Actualitza de forma automàtica la llista d\'aplicacions dels dipòsits Només en wifi + Actualitza automàticament les llistes d\'aplicacions només en wifi Notifica-ho + Avisa\'m quan hi hagi noves actualitzacions Actualitzacions + Mostra les aplicacions noves o actualitzades periòdicament Resultats de la cerca Detalls de l\'aplicació No s\'ha trobat l\'aplicació @@ -39,17 +47,20 @@ L\'adreça d\'un dipòsit té un aspecte com ara: http://f-droid.org/repoAfegeix un nou dipòsit Afegeix Anul·la + Sobreescriu Trieu el dipòsit que voleu suprimir Actualitza els dipòsits + Instal·lat Disponible Actualitzacions Hi ha 1 actualització disponible. Hi ha %d actualitzacions disponibles. Hi ha actualitzacions de l\'F-Droid disponibles Un moment si us plau - S\'està actualitzant la llista d\'aplicacions… + S\'està actualitzant la llista d\'aplicacions... S\'està obtenint l\'aplicació des de Adreça del dipòsit + Aquest repositori ja existeix. La llista de dipòsits ha canviat. La voleu actualitzar? Actualitza els dipòsits @@ -81,12 +92,17 @@ La voleu actualitzar? Aquesta aplicació depèn d\'altres aplicacions no lliures Pantalla Usuari expert + Activa el mode expert Cerca aplicacions Mode de sincronització de la base de dades + Estableix el valor de l\'etiqueta de sincronització de SQLite Compatibilitat de les aplicacions Versions incompatibles + Mostra versions d\'aplicacions que siguin incompatibles amb el dispositiu Root + Mostra aplicacions que necessiten privilegis de root Ignora la pantalla tàctil + Inclou sempre les aplicacions que necesiten de la pantalla tàctil Tot Novetats S\'ha actualitzat fa poc @@ -98,11 +114,14 @@ La voleu actualitzar? %1$s S\'està connectant a %1$s - S\'està comprovant la compatibilitat de les aplicacions amb el vostre dispositiu… + S\'està comprovant la compatibilitat de les aplicacions amb el vostre dispositiu... No es fa servir cap permís. Permisos de la versió %s Mostra els permisos + Mostra els permisos que necessita l\'aplicació No teniu cap aplicació disponible que pugui gestionar %s Vista compacta + Mostra només els noms de les aplicacions i els resums a la llista Tema + Escull un tema a utilitzar diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index e184664e1..213e2b4b8 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -7,14 +7,22 @@ Es sieht so aus, als sei dieses Paket mit Ihrem Gerät nicht kompatibel. Möchten Sie trotzdem versuchen es zu installieren? Sie versuchen eine vorherige Version einer bereits installierten Anwendung zu installieren. Dies kann zu Fehlverhalten der Anwendung und gegebenenfalls zu Datenverlust führen. Möchten Sie dennoch fortfahren? Version + %d Versionen verfügbar + %d Version verfügbar Heruntergeladene Anwendungen zwischenspeichern + Heruntergeladene Anwendungspakete auf der SD-Karte belassen Aktualisierungen Andere Letzte Aktualisierung der Paketquellen: %s niemals + Automatische Paketaktualisierung + Anwendungsliste automatisch aus den Paketquellen aktualisieren Nur über WLAN + Anwendungsliste nur über WLAN automatisch aktualisieren Benachrichtigen + Benachrichtigen, wenn neue Aktualisierungen verfügbar sind Aktualisierungsverlauf + Zeitraum in Tagen, für den neue bzw. aktualisierte Anwendungen angezeigt werden. Suchergebnisse Anwendungsdetails Keine passende Anwendung gefunden @@ -45,6 +53,7 @@ Die Adresse einer Paketquelle sieht etwa so aus: https://f-droid.org/repoÜberschreiben Zu entfernende Paketquelle auswählen Paketquellen aktualisieren + Installiert Verfügbar Aktualisierungen Eine Aktualisierung ist verfügbar. @@ -92,12 +101,17 @@ Sollen diese aktualisiert werden? Der Originalcode ist nicht völlig frei Anzeige Experte + Expertenmodus einschalten Anwendungen suchen Datenbanksynchronisierungsart + SQLite-Synchronisationsmodus einstellen Kompatibilität der Anwendung Inkompatible Versionen + Anwendungsversionen anzeigen, welche mit dem Gerät inkompatibel sind Root + Anwendungen anzeigen, die Root-Rechte benötigen Touchscreen ignorieren + Anwendungen die einen Touchscreen benötigen immer mit anzeigen Alle Was gibt es Neues Kürzlich Aktualisiert @@ -113,7 +127,10 @@ Sollen diese aktualisiert werden? Es werden keine Berechtigungen verwendet. Berechtigungen für Version %s Berechtigungen anzeigen + Eine Liste von Berechtigungen die von einer Anwendung benötigt werden anzeigen Es ist keine Anwendung installiert, die mit %s umgehen kann Kompakte Ansicht + Nur Namen und Kurzbeschreibung in der Anwendungsliste anzeigen Thema + Ein Thema welches benutzt werden soll auswählen diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index b7eef575a..219366daa 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -7,14 +7,22 @@ Φαίνεται ότι αυτό το πακέτο δεν είναι συμβατό με τη συσκευή σας. Θα θέλατε να δοκιμάσετε να το εγκαταστήσετε ούτως ή άλλως; Προσπαθείτε να υποβαθμίσετε αυτήν την εφαρμογή. Αν το κάνετε αυτό, μπορεί να προκληθούν προβλήματα στην εφαρμογή ή ακόμα και να χάσετε τα δεδομένα σας. Θέλετε να δοκιμάσετε να την υποβαθμίσετε ούτως ή άλλως; Έκδοση + %d διαθέσιμες εκδόσεις + %d διαθέσιμη έκδοση Αποθήκευση ληφθέντων εφαρμογών στην προσωρινή μνήμη + Διατήρηση ληφθέντων αρχείων apk στην κάρτα SD Ενημερώσεις Άλλα Τελευταίο σάρωμα αποθετηρίου: %s ποτέ + Αυτόματη Σάρωση Αποθετηρίου + Αυτόματη ενημέρωση της λίστας εφαρμογών από το αποθετήριο Μόνο σε wifi + Αυτόματη ενημέρωση της λίστας εφαρμογών μόνο σε wifi Ειδοποίηση + Ειδοποίηση για την ύπαρξη διαθέσιμων ενημερώσεων Ιστορικό ενημερώσεων + Ημέρες για να εμφανίζονται οι νέες/ενημερωμένες εφαρμογές Αποτελέσματα Αναζήτησης Λεπτομέρειες Εφαρμογής Δεν βρέθηκε τέτοια εφαρμογή @@ -41,13 +49,14 @@ Ακύρωση Επιλογή αποθετηρίου για διαγραφή Ενημέρωση αποθετηρίων + Εγκατεστημένο Διαθέσιμα Ενημερώσεις 1 διαθέσιμη ενημερώση. %d διαθέσιμες ενημερώσεις. Διαθέσιμες ενημερώσεις για το F-Droid Παρακαλώ περιμένετε - Ενημέρωση λίστα εφαρμογών… + Ενημέρωση λίστα εφαρμογών... Λήψη εφαρμογών από Διεύθυνση αποθετηρίου Η λίστα με τα χρησιμοποιούμενα αποθετήρια έχει αλλάξει. @@ -81,12 +90,17 @@ Αυτή η εφαρμογή εξαρτάται από άλλες μη-ελεύθερες εφαρμογές. Εμφάνιση Για Προχωρημένους + Ενεργοποίηση λειτουργίας για προχωρημένους Αναζήτηση εφαρμογών Λειτουργία συγχρονισμόυ της βάσης δεδομένων + Ορισμός τιμής για SQLite synchronous flag Συμβατότητα εφαμοργής Μη συμβατές εκδόσεις + Εμφάνιση εκδόσεων εφαρμογών που δεν είναι συμβατές με τη συσκευή Root + Εμφάνιση εφαρμογών που απαιτούν δικαιώματα root Αγνόησε την Οθόνη Επαφής + Να συμπεριλαμβάνονται πάντα εφαρμογές που απαιτούν οθόνη επαφής Όλα Τι νέο υπάρχει Πρόσφατα Ενημερωμένες @@ -98,11 +112,14 @@ %1$s Σύνδεση με %1$s - Έλεγχος συμβατότητας εφαρμογών με τη συσκευή σας… + Έλεγχος συμβατότητας εφαρμογών με τη συσκευή σας... Δεν χρησιμοποιείται καμία άδεια. Άδειες για την έκδοση %s Εμφάνιση αδειών + Εμφάνιση λίστας αδειών που χρειάζεται μια εφαρμογή Δεν έχεται καμία διαθέσιμη εφαρμογή που να μπορεί να χειριστεί %s Συμπτυγμένη Διάταξη + Εμφάνιση μόνο ονομάτων εφαρμογών και περίληψεων στη λίστα Θέμα + Επιλέξτε θέμα προς χρήση diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml new file mode 100644 index 000000000..13423d61a --- /dev/null +++ b/res/values-en-rGB/strings.xml @@ -0,0 +1,7 @@ + + + Version: + Enable + Add Key + Overwrite + diff --git a/res/values-eo/strings.xml b/res/values-eo/strings.xml index 1d61d54e6..29b7f290b 100644 --- a/res/values-eo/strings.xml +++ b/res/values-eo/strings.xml @@ -1,6 +1,8 @@ Versio + %d versioj disponeblaj + %d versio disponebla Ĝisdatigoj Sciigi Pri F-Droid @@ -17,6 +19,7 @@ Aldoni Rezigni Ĝisdatigi deponejojn + Instalitaj Disponeblaj Ĝisdatigoj Bonvolu atendi diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 996cdf9b9..5cbbcc4fe 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -7,14 +7,22 @@ Parece que este paquete no es compatible con tu dispositivo. ¿Quieres probar e instalarlo de todos modos? Estás intentando instalar una versión inferior de esta aplicación. Hacerlo puede derivar en mal funcionamiento o incluso pérdida de datos. ¿Quieres intentarlo de todos modos? Versión + %d versiones disponibles + %d versión disponible Caché de aplicaciones descargadas + Mantener los ficheros apk descargados en la SD card Actualizaciones Otros Último escaneo del repositorio: %s nunca + Escanear los repositorios automáticamente + Actualizar la lista de aplicaciones desde los repositorios automáticamente Sólo con wifi + Actualizar la lista de aplicaciones desde los repositorios automáticamente sólo con wifi Notificar + Notificar cuando haya actualizaciones disponibles Historial de actualizaciones + Días a mostrar apps nuevas/actualizadas Resultados de la búsqueda Detalles de la aplicación No se encontró la aplicación @@ -41,13 +49,14 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Cancelar Elige el repositorio a eliminar Actualizar repositorios + Instalado Disponible Actualizaciones 1 actualización disponible. %d actualizaciones disponibles. Actualizaciones de F-Droid disponibles Por favor, espera - Actualizando la lista de aplicaciones… + Actualizando la lista de aplicaciones... Obteniendo la aplicación de Dirección del repositorio La lista de repositorios usada ha cambiado. @@ -81,12 +90,17 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Esta aplicación depende de otras no libres Mostrar Experto + Activa el modo experto Buscar aplicaciones Modo síncrono de base de datos + Fija el valor del flag síncrono de SQLite Compatibilidad de aplicaciones Versiones incompatibles + Muestra versiones de aplicaciones que no sean compatibles con el dispositivo Root + Muestras las aplicaciones que requieren privilegios de root Ignorar pantalla táctil + Siempre incluir aplicaciones que requieren pantalla táctil Todos Novedades Recientemente actualizados @@ -98,11 +112,14 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo %1$s Conectando a %1$s - Comprobando la compatibilidad de las aplicaciones con tu dispositivo… + Comprobando la compatibilidad de las aplicaciones con tu dispositivo... No se usan permisos. Permisos para la versión %s Mostrar permisos + Mostrar una lista de los permisos que necesita una aplicación No tienes instalada ninguna aplicación que pueda manejar %s Diseño compacto + Mostrar sólo los nombres de las aplicaciones y resúmenes en la lista Tema + Escoger un tema a usar diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index b4680d751..17251f6f0 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -5,11 +5,17 @@ \'%s\'-rekin bat datorren aplikaziorik ez da aurkitu Bertsio berria zaharraren desberdina den gako batekin sinatuta dago. Bertsio berria instalatzeko, aurretik zaharra desinstalatu beharra dago. Mesedez, egizu eta saiatu berriro. (Kontutan izan desinstalatzean aplikazioak gordetako barne datuak ezabatuko direla) Bertsioa + %d bertsio eskuragarri + Bertsio %d eskuragarri Gorde cache-an deskargatutako aplikazioak + Gorde deskargatutako apk fitxategiak SD txartelean Eguneraketak Biltegiaren azken eskaneatzea: %s inoiz ez + Eskaneatu biltegiak automatikoki + Eguneratu aplikazio-zerrenda biltegiarekin automatikoki Jakinarazi + Jakinarazi eguneraketa berriak eskuragarri daudenean Eguneratu historia F-Droid-i buruz Jatorrian Aptoide-n oinarritua. @@ -29,13 +35,14 @@ GNU GPLv3 lizentziapean argitaratua. Utzi Aukeratu biltegia ezabatzeko Eguneratu biltegiak + Instalatuta Eskuragarri Eguneraketak Eguneraketa 1 eskuragarri. %d eguneraketa eskuragarri. F-Droid eguneraketak eskuragarri Mesedez itxaron - Aplikazio-zerrenda eguneratzen… + Aplikazio-zerrenda eguneratzen... Aplikazioa eskuratzen hemendik Biltegiaren helbidea Erabilitako biltegien zerrenda aldatu egin da. @@ -61,19 +68,24 @@ Eguneratu nahi dituzu? Tämä ohjelma sisältää mainontaa Bistaratu Aditua + Gaitu aditu modua Bilatu aplikazioak Datu-base modu sinkronoa + Ezarri SQLite-ren bandera sinkronoaren balioa Aplikazioen bateragarritasuna Root + Erakutsi root baimenak behar dituzten aplikazioak Ezikusi egin ukipen-pantailari + Sartu ukipen-pantaila behar duten aplikazioak beti Guztia Zer da berria Azkenaldian eguneratua %1$s(e)ra konektatzen - Aplikazioak zure gailuarekin bateragarriak diren egiaztatzen… + Aplikazioak zure gailuarekin bateragarriak diren egiaztatzen... Ez da baimenik erabiltzen. %s bertsioarentzako baimenak Erakutsi baimenak + Bistaratu aplikazio batek behar dituen baimenen zerrenda Diseinu trinkoa diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml index 1d4c28915..d5eba10c2 100644 --- a/res/values-fa/strings.xml +++ b/res/values-fa/strings.xml @@ -7,14 +7,22 @@ به‌نظر می‌رسد این بسته با دستگاه شما هماهنگ نیست. آیا می‌خواهید آن را به هر قیمتی آزمایش و نصب کنید؟ شما در حال قدیمی‌کردن و کاهش درجهٔ این برنامه هستید. انجام چنین کاری ممکن منجر به خرابی یا از دست رفتن داده‌های شما شود. آیا می‌خواهید سعی کنید این برنامه را به هر قیمتی قدیمی کنید؟ نسخه + %d نسخه موجود است + %d نسخه موجود است میانگیری برنامه‌های دریافت‌شده + نگه‌داشتن پرونده‌های APK دریافت‌شده در کارت SD به‌روزرسانی‌ها دیگر آخرین اسکن مخزن: %S هیچ‌گاه + بررسی خودکار مخزن برنامه‌ها + به‌روزرسانی فهرست برنامه‌ها از مخازن به‌صورت خودکار فقط هنگام اتصال وای‌فای + به‌روزرسانی فهرست برنامه به صورت خودکار فقط هنگام اتصال وای‌فای مطلع‌سازی + هنگامی که به‌روزرسانی‌های جدیدی موجود بود من را مطلع کن به‌روزرسانی تاریخچه + روزهای نمایش برنامه‌های جدید/به‌روزشده جستجوی نتایج مشخصات برنامه چنین برنامه‌ای یافت نشد @@ -41,13 +49,14 @@ فسخ انتخاب مخزن برای حذف به‌روزرسانی مخازن + نصب‌شده موجود به‌روزرسانی‌ها ۱ به‌روزرسانی موجود است. %d به‌روزرسانی موجود است. به‌روزرسانی‌های F-Droid موجود هستند لطفاً صبر کنید - به‌روزرسانی فهرست برنامه‌ها… + به‌روزرسانی فهرست برنامه‌ها... گرفتن برنامه از نشانی مخزن فهرست مخزن‌ها تغییر یافته‌است. @@ -81,12 +90,17 @@ این برنامه به سایر برنامه‌های غیر آزاد وابسته است نمایش خارج‌سازی + فعال‌سازی حالت خارج‌سازی جستجوی برنامه‌ها حالت هماهنگی پایگاه داده‌ها + تنظیم مقدار پرچم «همزمانی» اس‌کیولایت هماهنگی برنامه نسخه‌های غیرهماهنگ + نسخه‌هایی از برنامه که ناهماهنگ با این دستگاه است را نمایش بده روت + برنامه‌هایی که نیازمند دسترسی روت هستند را نمایش بده صفحهٔ نمایش لمسی را در نظر نگیر + همیشه برنامه‌هایی که نیازمند صفحهٔ نمایش لمسی هستند را شامل کن همه چیزهای جدید اخیراً به‌روز شده @@ -98,11 +112,14 @@ %1$s اتصال به %1$s - بررسی سازگاری برنامه‌ها با دستگاه شما… + بررسی سازگاری برنامه‌ها با دستگاه شما... دسترسی‌ای استفاده نشده‌است. دسترسی‌های نسخهٔ %s نمایش دسترسی‌ها + فهرستی از دسترسی‌هایی که یک برنامه نیاز دارد را نشان بده شما برنامه‌ای که بتواند %s را مدیریت کند ندارید طرح‌بندی فشرده + فقط نام‌ها و خلاصه‌ها را در فهرست نمایش بده پوسته + پوسته‌ای برای استفاده انتخاب کنید diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml index 367475645..30c1de051 100644 --- a/res/values-fi/strings.xml +++ b/res/values-fi/strings.xml @@ -5,11 +5,17 @@ \'%s\':ään täsmääviä sovelluksia ei löytynyt Uusi versio on allekirjoitettu eri avaimella kuin vanha. Asentaaksesi uuden version, vanha täytyy poistaa ensin. Tee tämä ja yritä uudelleen. (Huomaa, että poistaminen poistaa kaiken sovelluksen sisäisen datan) Versio + %d versiota saatavilla + %d versio saatavilla Säilytä ladatut sovellukset välimuistissa + Pidä ladatut apk-tiedostot SD-kortilla Päivitykset Viimeisin sovelluslähteiden skannaus: %s ei koskaan + Automaattinen sovelluslähteen skannaus + Päivitä sovelluslista sovelluslähteistä automaattisesti Huomauta + Ilmoita kun uusia päivityksiä on saatavilla Päivityshistoria Hakutulokset Tietoa F-Droidista @@ -27,13 +33,14 @@ Peruuta Valitse sovelluslähde, jonka tahdot poistaa Päivitä sovelluslähteet + Asennettu Saatavilla Päivityksiä 1 päivitys saatavilla. %d päivitystä saatavilla. F-Droid: Päivityksiä saatavilla Odota hetki - Päivitetään sovelluslistaa… + Päivitetään sovelluslistaa... Haetaan sovellusta lähteestä Sovelluslähteen osoite Lista käytetyistä sovelluslähteistä on muuttumut. @@ -60,15 +67,21 @@ Tahdotko päivittää ne? Lataus peruutettu Näyttö Asiantuntija + Ota käyttöön asiantuntija-tila Etsi sovelluksia Tietokannan synkronointi-tila + Aseta SQLiten synkrooninen lippu Sovellusten yhteensopivuus Yhteensopimattomat versiot + Näytä ohjelmaversiot jotka eivät ole yhteensopivia laitteesi kanssa Root + Näytä sovellukset, jotka vaativat root-oikeudet Älä välitä kosketusnäytöstä + Sisällytä aina sovellukset, jotka vaativat kosketusnäytön Kaikki Uutta Viimeaikoina päivitetty - Tarkistetaan ohjelman yhteensopivuutta laitteesi kanssa… + Tarkistetaan ohjelman yhteensopivuutta laitteesi kanssa... Teema + Valitse käytettävä teema diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 55e0d974a..aad768058 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -7,14 +7,22 @@ Il semble que ce paquet ne soit pas compatible avec votre appareil. Voulez-vous quand même tenter de l\'installer ? Vous essayez de revenir à une ancienne version de cette application. Cela peut causer des problèmes de fonctionnement ou des pertes de données. Voulez-vous tout de même revenir à une ancienne version? Version + %d versions disponibles + %d version disponible Stocker les applications téléchargées sur l\'appareil + Garder les fichiers apk téléchargés sur la carte SD Mises à jour Autres Dernière analyse du dépôt : %s jamais + Balayage automatique du dépôt + Mettre à jour automatiquement la liste d\'applications à partir des dépôts Seulement par WiFi + Mettre à jour automatiquement la liste d\'applications seulement via WiFi Notifier + Avertir quand de nouvelles mises à jour sont disponibles Historique des mises à jour + Jours pour présenter les applications nouvelles/mises à jour Résultats de la recherche Détails de l\'application Pas d\'application trouvée @@ -41,13 +49,14 @@ L\'URL d\'un dépôt ressemble à ceci : http://f-droid.org/repo Annuler Choisissez le dépôt à supprimer Mettre à jour les dépôts + Installée Disponible Mises à jour 1 mise à jour est disponible. %d mises à jour sont disponibles. Des mises à jour F-Droid sont disponibles Patientez - Mise à jour de la liste d\'applications… + Mise à jour de la liste d\'applications... Réception d\'application de Adresse du dépôt La liste des dépôts utilisés a changé. @@ -81,12 +90,17 @@ Voulez-vous les mettre à jour ? Cette application dépend d\'autres applications non libres Affichage Expert + Activer le mode expert Rechercher des applications Mode de synchronisation à la base de données + Régler la valeur de la synchronisation SQLite Compatibilité de l\'application Versions incompatibles + Afficher les version des applis qui ne sont pas compatibles avec votre appareil Root + Montrer les applications qui requièrent les privilèges root Ignorer l\'écran tactile + Toujours inclure les applications qui nécessitent un écran tactile Tout Quoi de neuf ? Mis à jour récemment @@ -102,7 +116,10 @@ Voulez-vous les mettre à jour ? Aucune autorisation n\'est utilisée. Autorisations pour la version %s Afficher les autorisations + Afficher la liste des autorisations que nécessite l\'application Vous n\'avez aucune application installée pour gérer %s Affichage compact + Afficher seulement les noms d\'applications et les résumés dans la liste Thème + Sélectionner un thème à utiliser diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml index 3b28894da..a68ac09e5 100644 --- a/res/values-gl/strings.xml +++ b/res/values-gl/strings.xml @@ -7,14 +7,22 @@ Parece que este paquete non é compatíbel co seu dispositivo. Quere facer a proba e instalalo aínda así? Está tentando instalar unha versión anterior. Isto pode supoñer un mal funcionamento ou perda de datos. Quere tentalo de todas maneiras? Versión + %d versións dispoñíbeis + %d versión dispoñíbeis Memorizar as aps descargadas + Gardar os ficheiros apk descargados na tarxeta SD Actualizacións Outro Último escaneado do repositorio: %s nunca + Escaneado automático dos repositorios + Actualizar automaticamente a lista de aps do repositorio Soamente no wifi + Actualizar as listas de aps automaticamente soamente cando hai wifi Notificar + Avisarme cando estean dispoñíbeis novas actualizacións Histórico de actualizacións + Días para amosar aps novas/actualizadas Resultados da busca Detalles da ap Non se atopa tal ap @@ -45,13 +53,14 @@ Un enderezo a un repositorio sería algo Cancelar Escoller o repositorio que retirar Actualizar repositorios + Instalado Dispoñíbel Actualizacións 1 Actualización dispoñíbel %d actualizacións dispoñíbeis Actualizacións de F-Droid dispoñíbeis Agarde por favor - Actualizando a lista de aplicativos… + Actualizando a lista de aplicativos... Obtención do aplicativo desde Enderezo do repositorio Cambiou a lista de repositorios usados. @@ -85,12 +94,17 @@ Quere actualizalos? Esta ap depende doutras aps non libres Amosar Experto + Activar o modo experto Buscar aplicativos Modo de sincronización da base de datos + Estabelece o valor da marca de sincronización de SQLite Compatibilidade de aplicativos Versións incompatíbeis + Amosar versións de aps que son incompatíbeis co dispositivo Root + Amosar aplicativos que requiren privilexios de root Ignorar a pantalla táctil + Incluír sempre as aps que requiren pantalla táctil Todos Qué novidades hai Actualizado recentemente @@ -106,7 +120,10 @@ Quere actualizalos? Non se usan permisos Permisos para a versión %s Amosar permisos + Presentar a lista dos permisos que precisa unha ap Non ten ningunha ap dispoñíbel que poida manexar %s Deseño compacto + Amosar unicamente os nomes das aps e resumos na lista Tema + Escolla un tema diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index fde3ceaff..38d0b678b 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -7,14 +7,22 @@ Sembra che questo pacchetto non sia compatibile con il tuo dispositivo. Vuoi provare comunque ad installarlo? Stai provando a passare ad una versione precedente di questa applicazione. Potresti avere malfunzionamenti e perdita di dati. Vuoi installarla comunque? Versione + %d versioni disponibili + %d versione disponibile Cache applicazioni scaricate + Salva su SD i file apk scaricati Aggiornamenti Altro Ultima scansione repository: %s mai + Scansione repository automatica + Aggiorna automaticamente l\'elenco applicazioni Solo su wifi + Aggiorna liste app automaticamente solo su wifi Avviso + Avvisa quando sono disponibili nuovi aggiornamenti Aggiorna i repository + Giorni per mostrare app nuove/da aggiornare Risultati Ricerca Dettagli App Nessuna app corrispondente trovata @@ -41,13 +49,14 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo Annulla Rimuovi repository Aggiorna i repository + Installato Disponibile Aggiornamenti 1 aggiornamento disponibile. %d aggiornamenti disponibili. Aggiornamenti per F-Droid Disponibili Attendere prego - Aggiornamento elenco applicazioni… + Aggiornamento elenco applicazioni... Scaricamento applicazione da Indirizzo repository L\'elenco dei repository in uso è cambiato. @@ -81,12 +90,17 @@ Vuoi aggiornarlo? Questa app dipende da applicazioni non libere Mostra Esperto + Abilita la modalità avanzata Ricerca applicazioni Modalità di sincronizzazione database + Impostazione del flag di sincronizzazione di SQLite Compatibilità applicazioni Versioni incompatibili + Mostra versioni delle app incompatibili con questo dispositivo Amministratore + Mostra le applicazioni che richiedono i privilegi di amministrazione Ignora il Touchscreen + Includi sempre le applicazioni che richiedono il touchscreen Tutte Novità Aggiornate di Recente @@ -98,11 +112,14 @@ Vuoi aggiornarlo? %1$s Connessione a %1$s - Controllo compatibilità applicazioni con il tuo dispositivo… + Controllo compatibilità applicazioni con il tuo dispositivo... Non viene usata alcuna autorizzazione. Autorizzazioni per la versione %s Mostra autorizzazioni + Mostra la lista di autorizzazioni necessarie per un\'app Non hai alcuna app disponibile che può gestire %s Layout Compatto + Mostra solo nomi e sintesi delle app nella lista Tema + Scegli un tema da usare diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index 28be1501c..b4657fd03 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -5,12 +5,19 @@ \'%s\' 와 일치하는 응용프로그램을 찾을 수 없습니다. 이 응용 프로그램의 다운그레이드 하려고 합니다. 이전 버전을 설치할 경우, 응용 프로그램에 데이터가 손상되거나 오작동이 발생할 수 있습니다. 정말로 다운그레이드하시겠습니까? 버전 + %d개의 버전을 사용할 수 있습니다. + %d개의 버전을 사용할 수 있습니다. 다운로드된 설치파일 저장 + SD카드에 다운로드된 설치파일을 보관 업데이트 기타 마지막 저장소 검색: %s + 자동으로 저장소 검색 + 자동으로 저장소의 앱 목록을 갱신합니다. Wi-Fi 연결 시에만 + Wi-Fi에 연결되어 있을 때만 자동으로 앱 목록을 갱신합니다. 알림 + 새로운 업데이트가 가능할 때 알림 이력 업데이트 검색 결과 앱 상세정보 @@ -34,12 +41,13 @@ 취소 제거할 저장소 선택 저장소 업데이트 + 설치됨 업데이트 1개의 업데이트를 사용할 수 있습니다. %d개의 업데이트를 사용할 수 있습니다. F-Droid 업데이트를 사용할 수 있습니다. 잠시만 기다려주세요 - 응용 프로그램 목록 업데이트중… + 응용 프로그램 목록 업데이트중... 에서 응용프로그램 가져오기 저장소 주소 사용된 저장소의 목록이 변경되었습니다. @@ -69,11 +77,14 @@ 이 응용프로그램은 활동을 추적하여 리포트를 보고합니다. 표시 전문가 + 전문가 모드 사용 응용 프로그램 검색 데이터베이스 동기화 모드 응용 프로그램 호환성 호환되지 않는 버전 + Root 권한이 필요한 앱을 보여줍니다. 터치스크린 무시 + 터치스크린을 요구하는 앱을 포함합니다 전체 새로운 기능 최근 업데이트 @@ -87,7 +98,10 @@ 사용된 권한이 없습니다. %s 버전에 대한 권한 권한 표시 + 응용 프로그램이 필요한 권한의 목록을 표시 %s을(를) 처리할 수 있는 응용프로그램이 없습니다. 컴팩트 레이아웃 + 목록에 웹이름과 요약정보만 표시합니다. 테마 + 사용할 테마를 선택하세요. diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index bd501e332..224f4be4d 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -7,14 +7,22 @@ Det ser ut til at denne pakken ikke er kompatibel med ditt utstyr. Vil du prøve å installere det likevel? Du prøver å nedgradere denne applikasjonen. Dette kan føre til at applikasjonen henger, og du kan til og med miste dine data. Vil du prøve å nedgradere likevel? Versjon + %d versjoner tilgjengelig + %d versjon tilgjengelig Lagre nedlastede applikasjoner i buffer + Behold nedlastede apk-filer på minnekortet Oppdateringer Andre Forrige registeroppdatering: %s aldri + Automatisk registeroppdatering + Oppdater applikasjonsliste fra register automatisk Bare på trådløst + Oppdater applikasjoner automatisk kun på trådløst Varsle + Varsle når nye oppdateringer er tilgjengelige Oppdater historie + Dager for visning av nye/oppdaterte applikasjoner Søkeresultater Programdetaljer Program ikke funnet @@ -37,13 +45,14 @@ Lisensiert GNU GPLv3. Avbryt Velg register du vil fjerne Oppdater registrene + Installert Tilgjengelig Oppdateringer 1 oppdatering tilgjengelig. %d oppdateringer tilgjengelig. F-Droid: Oppdateringer tilgjengelig Vennligst vent - Oppdaterer applikasjonsliste… + Oppdaterer applikasjonsliste... Henter program fra Registeradresse Listen over brukte register har endret seg. Vil du oppdatere dem? @@ -76,12 +85,17 @@ Lisensiert GNU GPLv3. Dette programmet avhenger av andre ufrie applikasjoner Vis Ekspert + Skru på ekspertmodus Søk i programliste Modus for databasesynkronisering + Sett verdien for SQLites \"synchronous\"-flagg Programstøtte Ukompatible versjoner + Vis versjoner av applikasjoner som ikke er kompatible med ditt utstyr Rot + Vis applikasjoner som krever rottilgang Ignorer pekeskjerm + Inkluder alltid applikasjoner som krever pekeskjerm Alle Det som er nytt Nylig oppdatert @@ -93,11 +107,14 @@ Lisensiert GNU GPLv3. %1$s Kobler til %1$s - Sjekker programstøtte for ditt utstyr… + Sjekker programstøtte for ditt utstyr... Krever ingen tillatelser. Tillatelser for versjon %s Vis tillatelser + Vis liste over tillatelser en applikasjon trenger Du har ingen tilgjengelige applikasjoner som kan håndtere %s Kompakt layout + Vis kun navn og sammendrag på applikasjoner i listen Utseende + Velg et utseende diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index 6eeb3910a..3bb122e70 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -7,14 +7,22 @@ Dit pakket is niet verenigbaar met uw apparaat. Wilt u het alsnog proberen te installeren? U probeert deze applicatie te degraderen naar een oudere versie. Als u dit doet uw data kan corrupt of verloren raken. Wilt u dit alsnog uitvoeren? Versie + %d versies beschikbaar + %d versie beschikbaar buffer gedownloade apps + Bewaar gedownloade apk-bestanden op de SD-kaart Updates Andere Laatste bronnen scan: %s nooit + Scan bronnen automatisch + app-lijst automatisch bijwerken Allen op wifi + Vernieuw app-lijst alleen op wifi automatisch Verwittigen + Verwittigen wanneer nieuwe updates beschikbaar zijn Vernieuw historie + Dagen om nieuwe/verbeterde apps te laten zien Zoekresultaten App Details Geen dergelijke app gevonden @@ -44,13 +52,14 @@ Een bron-adres ziet er ongeveer Annuleren Kies bron om te verwijderen Vernieuw bronnen + Geïnstalleerd Beschikbaar Updates 1 vernieuwing is beschikbaar %d vernieuwingen zijn beschikbaar F-Droid Vernieuwingen Beschikbaar Even geduld aub - Applicatie-lijst vernieuwen… + Applicatie-lijst vernieuwen... downloaden applicatie van Bron-adres De lijst van gebruikte bronnen is veranderd. @@ -84,12 +93,17 @@ Wilt u ze vernieuwen? Deze app vereist andere niet-vrije apps Laat zien Expert + Ga in expert-modus Zoek applicaties Database sync-modus + Zet de waarde van SQLite\'s synchronisatie-vlag Applicatie verenigbaarheid Onverenigbare versies + Laat versies van apps die onverenigbaar zijn met het apparaat Root + Laat apps zien die root-privileges vereisen Negeer aanraakscherm + Altijd apps die aanraakscherm vereisen weergeven Alles Wat is nieuw Recentelijk vernieuwd @@ -101,11 +115,14 @@ Wilt u ze vernieuwen? %1$s Connecteren naar %1$s - Controleer app compatibiliteit met uw apparaat… + Controleer app compatibiliteit met uw apparaat... Geen permissies worden gebruikt Permissies voor versie %s Laat permissies zien + Laat een lijst met benodigde permissies van de app zien U hebt geen beschikbare app die %s kan verwerken Compacte Layout + Laat alleen namen en samenvattingen van apps zien in de lijst Thema + Kies een thema om te gebruiken diff --git a/res/values-pl/array.xml b/res/values-pl/array.xml index 61ff85ad2..103711e39 100644 --- a/res/values-pl/array.xml +++ b/res/values-pl/array.xml @@ -8,8 +8,8 @@ Codziennie - Dark - Light + Ciemny + Jasny Wyłączone (niebezpieczne) diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index e328439bd..3fc646fea 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -4,18 +4,28 @@ Znaleziono jedną pasującą aplikację \'%s\': Nie znaleziono żadnych pasujących aplikacji \'%s\' Nowa wersja jest podpisana innym kluczem niż poprzednia. Aby ją zainstalować należy najpierw usunąć tę starą. Zrób to i spróbuj ponownie. (Proszę pamiętać, że deinstalacja spowoduje usunięcie wszystkich danych przechowywanych przez aplikację) + Wygląda na to, że ta aplikacja nie jest kompatybilna z twoim urządzeniem. Spróbować mimo to? Wersja + %d dostępnych wersji + %d dostępna wersja Buforuj pobrane aplikacje + Przechowuj pobrane pliki apk na karcie SD Aktualizacje Inne Ostatnie uaktualnienie listy aplikacji: %s nigdy + Automatycznie skanuj repozytoria + Automatycznie uaktualnij listę aplikacji z repozytorium Tylko przez wifi + Aktualizuj automatycznie tylko przez wifi Powiadom + Powiadamiaj, gdy dostępne będą nowe aktualizacje Historia aktualizacji Wyniki wyszukiwania Nie znaleziono takiej aplikacji O F-Droid + Oryginalnie bazowany na Aptoide. +Wydany na licencji GNU GPLv3. Strona internetowa: Email: Wersja: @@ -29,14 +39,22 @@ Dodaj nowe repozytorium Dodaj Anuluj + Dodaj klucz + Nadpisz Wybierz repozytorium które chcesz usunąć Aktualizuj repozytoria + Zainstalowano Dostępne Aktualizacje + Dostępne jest 1 uaktualnienie. + Dostępnych jest %d uaktualnień + Uaktualnienie F-Droid jest dostępne Proszę czekać - Aktualizowanie listy aplikacji… + Aktualizowanie listy aplikacji... Pobieranie aplikacji z Adres repozytorium + odcisk (opcjonalnie) + Repozytorium już istnieje! Lista wykorzystywanych repozytoriów uległa zmianie. Czy chcesz je zaktualizować? Aktualizuj repozytoria @@ -55,20 +73,38 @@ Czy chcesz je zaktualizować? Strona internetowa Problemy Kod żródłowy + Uaktualnij Złóż datek Wersja %s została zainstalowana Nie zainstalowano Pobrany plik jest uszkodzony Anulowano pobieranie + Ta aplikacja zawiera reklamy + Ta aplikacja śledzi twoje działania + Ta aplikacja promuje niewolne dodatki + Ta aplikacja promuje niewolne usługi + Ta aplikacja wymaga innych, niewolnych aplikacji Ekspert + Uruchom tryb eksperta Wyszukaj aplikacje Tryb synchronizacji bazy danych + Ustaw synchronizację flagi SQLite Kompatybilność aplikacji Root + Pokaż aplikacje wymagające uprawnień root Wszystkie Co nowego Ostatnio zaktualizowane + Pobieranie %2$s / %3$s(%4$d%%) z %1$s + Przetwarzanie aplikacji %2$d / %3$d z %1$s Trwa łączenie z %1$s + Sprawdzanie kompatybilności aplikacji z urządzeniem... + Uprawnienia dla wersji %s Wyświetl uprawnienia + Wyświetlaj listę uprawnień wymaganych przez aplikację + Widok kompaktowy + Wyświetlaj tylko nazwy i podsumowania aplikacji + Motyw + Wybierz motyw diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index c51a79245..858e5f162 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -7,14 +7,22 @@ Aparentemente esse pacote não é compatível com o seu dispositivo. Quer tentar instalá-lo mesmo assim? Você está tentando desatualizar este aplicativo. Isso pode causar mal funcionamento e eventualmente perda de dados. Você quer tentar desatualizá-lo mesmo assim? Versão + %d versões disponíveis + %d versão disponível Cache de aplicativos baixado + Manter no cartão SD os arquivos apk baixados Atualizações Outro Última consulta aos repositórios: %s nunca + Consulta automática aos repositórios + Atualizar a lista de aplicativos automaticamente a partir dos repositórios Só com wifi + Atualizar a lista de aplicativos somente via wifi Notificar + Notificar quando novas atualizações estiverem disponíveis Atualizar histórico + Dias para mostrar apps novos/atualizados Resultados da Pesquisa Detalhes do Aplicativo Nenhum aplicativo encontrado @@ -41,13 +49,14 @@ Um endereço do repositório é algo similar a isto: http://f-droid.org/repoCancelar Escolha o repositório para remover Atualizar repositórios + Instalado Disponível Atualizações 1 atualização disponível. %d atualizações disponíveis. Atualizações do F-Droid Disponíveis Aguarde - Atualizando a lista de aplicativos… + Atualizando a lista de aplicativos... Baixando aplicativo de Endereço do repositório A lista de repositórios usados mudou. @@ -81,12 +90,17 @@ Você deseja atualizá-los? Este aplicativo depende de aplicativos não-livres Exibição Especialista + Ativar modo especialista Pesquisar aplicativos Modo de sincronia do banco de dados + Definir o valor da flag de sincronia do SQLite Compatibilidade de aplicativo Versões incompatíveis + Mostrar versões de aplicativos incompatíveis com o dispositivo Root + Mostrar aplicativos que requerem privilégios de root Ignorar tela sensível ao toque + Sempre incluir aplicativos que requerem tela sensível ao toque Todos O que há de novo Atualizado Recentemente @@ -98,11 +112,14 @@ Você deseja atualizá-los? %1$s Conectando-se a %1$s - Verificando compatibilidade de aplicativos com o seu dispositivo… + Verificando compatibilidade de aplicativos com o seu dispositivo... Nenhuma permissão utilizada. Permissões para a versão %s Mostrar permissões + Mostrar uma lista de permissões que um aplicativo requer Você não tem aplicativo instalado que lide com %s Leiaute compacto + Mostrar só nomes de aplicativos e sumários na lista Tema + Escolha que tema utilizar diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml index 40c1a4fe3..d5cd2dfbf 100644 --- a/res/values-ro/strings.xml +++ b/res/values-ro/strings.xml @@ -4,9 +4,15 @@ Sa gasit o aplicatie potrivita cu %s\' Nu exita aplicatii potrivite cu %s\': Versiune + Versiunile %d disponibile + Versiunea %d disponibila Istoric aplicatii descarcate + Patrati fisierele apk descarcate pe cardul SD Noutati + Scanare versiuni noi + Actualizare aplicatie automata Notificare + Notificare cand exista versiuni noi Despre F-Droid Bazat pe Aptoide. Distribuit sub licenta GNU GPLv3. @@ -23,8 +29,9 @@ Distribuit sub licenta GNU GPLv3. Anuleaza Alegeti depozitul pentru stergere Actualizare depozit aplicatii + Instalat Disponibil Actualizare - Asteptati … - Se actualizeaza lista … + Asteptati ... + Se actualizeaza lista ... diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 225e618e9..3064836d7 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -6,12 +6,19 @@ Новая версия подписана ключом отличным от старого. Для установки новой версии, сначала нужно удалить старую программы. А потом попробовать снова. (Замечание: при удалении программы будут удалены все её данные) Вы пытаетесь установить более старую версию приложения. Это может привести к его некорректной работе и даже потере данных. Вы уверены, что хотите продолжить? Версия + версий доступно - %d + %d версия доступна Кешировать загруженные приложения + Сохранять загруженные apk файлы на SD карте Обновления Обновлено: %s никогда + Автоматически сканировать репозиторий + Обновлять список приложений автоматически Уведомление + Сообщать при появлении обновлений История обновлений + Сколько дней показывать новые/обновлённый приложения Результаты поиска Описание приложения Приложение не найдено @@ -38,12 +45,13 @@ Отменить Удалить репозиторий Обновить репозитории + Установлено Доступно Обновления Доступно 1 обновление. Обновлений доступно - %d. Подождите - Список приложений обновляется… + Список приложений обновляется... Взять приложение из Адрес репозитория Список репозиториев изменился. @@ -69,11 +77,15 @@ Загрузка остановлена Вид Эксперт + Включить режим эксперта Найти приложения Режим синхронизации базы + Установить флаг синхронизации SQLite Совместимость приложений Суперпользователь + Показывать приложения требующие root-привилегий Игнорировать Тачскрин + Всегда включать приложения требующие тачскрин Все Что Нового Недавно обновлённые @@ -82,9 +94,11 @@ %1$s Соединение с %1$s - Проверка совместимости приложений с устройством… + Проверка совместимости приложений с устройством... Разрешений не требуется. Разрешения для версии %s Показывать разрешения + Показывать список разрешений, необходимых приложению Компактный вид + Показывать в списке только названия и краткие описания приложений diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index 304697388..361dba0aa 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -5,9 +5,15 @@ Nobena aplikacija ne ustreza \'%s\' Nova različica je overovljena z drugim ključem kot starejša. V primeru, da želite namestiti novejšo različico morate najprej odstraniti staro. Poskusite ponovno, ko ste to naredili. (Bodite pozorni na dejstvo, da bodo zaradi odstranitve izbrisani vsi notranji podatki shranjeni v aplikaciji) Različica + %d različic na razpolago + %d različica na razpolago Predpomnilnik naloženih aplikacij + Shrani naložene datoteke apk na kartico SD Posodobitve + Samodejni pregled skladišč + Samodejno posodobi spisek aplikacij iz skladišč Opozorilo + Opozori na posodobitve Päivityshistoria Izvorno osnovan na Aptoide. Izdan z licenco GNU GPLv3. @@ -25,10 +31,11 @@ Izdan z licenco GNU GPLv3. Prekliči Odstrani skladišče Posodobi skladišča + Nameščeno Na razpolago Posodobitve Počakajte prosim - Poteka posodobitev spiska aplikacij … + Poteka posodobitev spiska aplikacij ... Prejem aplikacije iz Naslov skladišča Spisek uporabljenih skladišč se je spremenil. @@ -51,10 +58,14 @@ Ga želite posodobiti? Prenos je preklican Tämä ohjelma sisältää mainontaa Napredno + Vključi napredni način Iskanje aplikacij Način sinhronizacije baze podatkov + Nastavitev zastavice za sinhronost v SQLite Združljivost aplikacij Yhteensopimattomat versiot + . Skrbnik + Pokaži aplikacije, ki zahtevajo skrbniške pravice . diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index bb7a4516e..596a8efe5 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -7,14 +7,22 @@ Изгледа да овај пакет није компатибилан са вашим уређајем. Да ли желите да га све једно инсталирате? Тренутно покушавате да инсталирате старију верзију ове апликације. То може да доведе до кварова и губитка података. Да ли сте сигурни да желите да инсталирате старију верзију? Верзија + %d верзије/верзија на располагању + %d верзија на располагању Чувај скинуте апликације + Чувај скинуте apk датотеке на СД картици Ажурирање Druga Задње скенирање ризнице: %s никада + Аутоматско скенирање ризница + Аутоматски ажурирај листу апликација Само на бежичној мрежи + Аутоматски ажурирај листе апликација само на бежичној мрежи Обавести + Обавести кад су нове верзије на располагању Претходна ажурирања + Колико дана приказивати нове/ажуриране апликације Резултати Претраге Детаљни подаци за Апликацију Та апликација не постоји @@ -41,13 +49,14 @@ Поништи Изабери ризницу за уклањање Ажурирај ризнице + Инсталиране На располагању Нове верзије 1 нова верзија на располагању. %d нове/нових верзија на располагању Ажурирање Ф-Дроида на располагању. Сачекајте - Ажурира се листа апликација… + Ажурира се листа апликација... Скида се апликација са Адреса ризнице Промењена је листа ризница у употреби. @@ -80,11 +89,15 @@ За ову апликацију су потребни плаћени додаци Прикажи Стручни + Омогући стручни режим Претрага апликација Режим синхронизације базе података + Унесите вредност за SQLite синхрону заставу Компатибилност апликације Рут + Приказати апликације које захтевају рут привилегије Игнориши Додирни Екран + Увек приказати апликације које захтевају додирни екран Све Ново Недавно Ажурирано @@ -96,10 +109,12 @@ %1$s Повезивање са %1$s - Проверава се да ли је апликација компатибилна са вашим уређајем… + Проверава се да ли је апликација компатибилна са вашим уређајем... Не захтевају се никакве дозволе. Дозволе за верзију %s Прикажи дозволе + Приказати листу дозвола неопходних за апликацију Немате инсталирану апликацију за %s Компактни Распоред + Само приказати имена и сажете описе апликација на лист diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index 236956d02..9128e8f9b 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -7,14 +7,22 @@ Det verkar som att detta program inte är kompatibelt med enheten. Vill ni försöka installera det ändå? Du försöker nedgradera detta program. Detta kan få det att fungera felaktigt eller orsaka förlust av dina data. Vill du ändå försöka nedgradera? Version + %d versioner tillgängliga + %d version tillgänglig Cacha nerladdade appar + Behåll nerladdade apk-filer på SD-kortet Uppdateringar Andra Senaste förrådsavsökning: %s aldrig + Automatisk förrådsavsökning + Uppdatera applistan från förråd automatiskt Endast via WiFi + Uppdatera applistor automatiskt endast över wifi Avisering + Meddela mig när nya uppdateringar finns Uppdateringshistorik + Antal dagar att visa nya/uppdaterade appar Sökresultat Appdetaljer Ingen sådan app funnen @@ -39,17 +47,27 @@ En förrådsadress ser ut så här: https://f-droid.org/repo Lägg till nytt förråd Lägg till Avbryt + Aktivera + Lägg till nyckel + Skriv över Välj förråd att ta bort Uppdatera förråd + Installerade Tillgängliga Uppdateringar 1 uppdatering finns tillgänglig. %d uppdateringar finns tillgängliga. Uppdateringar för F-Droid tillgängliga Var vänlig vänta - Uppdaterar programlistan… + Uppdaterar programlistan... Hämtar program från Förrådsadress + fingeravtryck (valfritt) + Detta förråd existerar redan! + Detta förråd är redan installerat, detta kommer lägga till ny nyckelinformation. + Detta förråd är redan installerat, bekräfta att du vill återaktivera det. + Inkommande förråd är redan installerat och aktiverat! + Du måste först ta bort detta förråd innan du kan lägga till ett med en annan nyckel! Listan över förråd har ändrats. Vill du uppdatera dem? Uppdatera förråd @@ -79,14 +97,20 @@ Vill du uppdatera dem? Denna app främjar ofria tillägg Denna app främjar ofria nätverkstjänster Denna app beror på andra ofria appar + Källkoden från uppströms är inte fullständigt fri Visning Expert + Aktivera expertläge Sök program Databassynkroniseringsläge + Ställ in värdet på synchronous-flaggan i SQLite Programkompatibilitet Inkompatibla versioner + Visa versioner av appar som är inkompatibla med enheten Root + Visa appar som kräver root-rättigheter Ignorera touchscreen + Inkludera alltid appar som kräver touchscreen Alla Nyheter Nyligt uppdaterade @@ -102,7 +126,10 @@ Vill du uppdatera dem? Inga behörigheter används. Behörigheter för version %s Visa behörigheter + Visa en lista över behörigheter en app behöver Du har inte någon app tillgänglig för hantering av %s Kompakt layout + Visa endast appnamn och sammanfattningar i listan Tema + Välj ett tema att använda diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml index ab4e6a6c8..34a6b4cc1 100644 --- a/res/values-tr/strings.xml +++ b/res/values-tr/strings.xml @@ -7,14 +7,22 @@ Bu paket cihazınızla uyumlu değil gibi görünüyor. Yine de kurmayı denemek istiyor musunuz? Bu uygulamanın eski bir sürümüne dönmek üzeresiniz. Bu, uygulamanın yanlış çalışmasına ve hatta veri kaybına neden olabilir. Devam etmek istiyor musunuz? Sürüm + %d sürüm mevcut + %d sürüm mevcut İndirilen uygulamaları önbelleğe kaydet + İndirilen uygulamaları SD kartına kaydet Güncellemeler Diğer Son depo analizi: %s asla + Otomatik depo taraması + Uygulama listesini depolardan otomatik olarak güncelle Sadece WiFi ile + Uygulama listesini otomatik olarak sadece WiFi ile güncelle Bildirme + Yeni güncellemeler olduğunu bildir Güncelleme tarihçesi + Yeni/güncellenmiş uygulamaların gösterilecekleri gün sayısı Arama Sonuçları Uygulama Detayları Böyle bir uygulama bulunamadı @@ -41,13 +49,14 @@ Bir depo adresi şuna benzer: https://f-droid.org/repo İptal Kaldırılacak depoyu seç Depoları güncelle + Kurulu Mevcut Güncellemeler 1 güncelleme bulunmaktadır. %d güncelleme bulunmaktadır. F-Droid güncellemeleri bulunmaktadır Bekleyiniz - Uygulama listesi güncelleniyor… + Uygulama listesi güncelleniyor... Uygulama buradan alınıyor: Depo adresi Kullanılan depoların listesi değişti. @@ -81,12 +90,17 @@ Güncellemek ister misiniz? Bu uygulama özgür olmayan başka uygulamalara bağımlıdır Görüntüleme Uzman + Uzman modunu etkinleştir Uygulama ara Veritabanı eşleşme modu + SQLite\'ın senkronize flag değerini gir Uygulama uyumu Uyumsuz sürümler + Cihazınızla uyumsuz uygulama sürümlerini göster Root + Root yetkilerine gerek duyan uygulamaları göster Dokunmatik ekranı yok say + Dokunmatik ekran gerektiren uygulamaları daima ekle Tümü Yeni olanlar Yakın geçmişte güncellenen @@ -102,7 +116,10 @@ bağlanılıyor Hiçbir izin kullanılmıyor. %s sürümü için izinler İzinleri göster + Uygulamanın gerektirdiği izinlerin listesini göster %s unsurunu yönetecek hiçbir mevcut uygulamanız yok Yoğun düzen + Listede sadece uygulama adlarını ve özetleri göster Tema + Kullanılacak temayı seç diff --git a/res/values-ug/strings.xml b/res/values-ug/strings.xml index ffb278803..612a5ab4a 100644 --- a/res/values-ug/strings.xml +++ b/res/values-ug/strings.xml @@ -7,14 +7,22 @@ بۇ بوغچا ئۈسكۈنىڭىز بىلەن ماسلاشمايدىغاندەك تۇرىدۇ، ئۇنى سىناپ ئورنىتىۋېرەمسىز؟ بۇ ئەپنىڭ دەرىجىسىنى تۆۋەنلىتىشنى سىناۋاتىسىز. بۇ مەشغۇلاتنى ئىجرا قىلىش داۋامىدا كاشىلا كۆرۈلۈشى ۋە سانلىق مەلۇماتلىرىڭىزنى يوقۇتۇپ قويۇشىڭىز مۇمكىن. ئۇنى سىناپ دەرىجىسىنى تۆۋەنلىتىۋېرەمسىز؟ نەشرى + %d نەشرى بار + %d نەشرى بار ئەپلەر غەملەككە چۈشۈرۈلدى + چۈشۈرگەن apk ھۆججەتلەرنى SD كارتىدا ساقلاپ قال يېڭىلانمىلار باشقا ئاخىرقى repo تەكشۈرۈش: %s ھەرگىز + ئاپتوماتىك repo تەكشۈرۈش + ئەپ تىزىمىنى خەزىنەدىن ئۆزلۈكىدىن يېڭىلا wifi دىلا + ئەپ تىزىمىنى wifi دىلا ئۆزلۈكىدىن يېڭىلا ئۇقتۇرۇش + يېڭى يېڭىلانمىلار بولسا ئەسكەرت يېڭىلاش تارىخى + يېڭى/يېڭىلانغان ئەپلەرنى كۆرسىتىدىغان كۈن سانى ئىزدەش نەتىجىلىرى ئەپ تەپسىلاتلىرى بۇنداق ئەپ تېپىلمىدى @@ -41,6 +49,7 @@ ۋاز كەچ چىقىرىۋېتىدىغان خەزىنەنى تاللاڭ خەزىنە يېڭىلا + ئورنىتىلغان ئىشلىتىشچان يېڭىلانمىلار 1 يېڭىلانما بار. @@ -81,12 +90,17 @@ بۇ ئەپ ھەقسىز بولمىغان باشقا ئەپلەرگە بېقىنىدۇ كۆرسەت ئالىي + ئالىي ھالەتنى قوزغات ئەپ ئىزدە ساندان قەدەمداش ھالەت + بۇ SQLite قەدەمداش بايراقىنىڭ قىممىتىنى تەڭشەيدۇ ئەپ ماسلىشىشچانلىقى ماسلاشمايدىغان نەشرىلىرى + ئەپلەرنىڭ ئۈسكۈنە بىلەن ماسلاشمايدىغان نەشرىلىرىنى كۆرسىتىدۇ Root + root ھوقۇقى زۆرۈر بولغان ئەپلەرنى كۆرسەت سەزگۈر ئېكرانغا پەرۋا قىلما + ھەمىشە سەزگۈر ئېكرانلىق ئەپلەرنى ئۆز ئىچىگە ئالىدۇ ھەممىسى يېڭىلىقلار يېقىنقى يېڭىلانغانلار @@ -102,7 +116,10 @@ ھېچقانداق ھوقۇق ئىشلەتمەيدۇ. %s نەشرىنىڭ ھوقۇقلىرى ھوقۇقلارنى كۆرسەت + ئەپكە زۆرۈر بولغان ھوقۇق تىزىمىنى كۆرسىتىدۇ سىز %s نى بىر تەرەپ قىلالايدىغان ھېچقانداق ئەپ ئورناتمىغان ئىخچام جايلاشتۇرۇش + تىزىمدا پەقەت ئەپ ئىسمى ۋە ئۈزۈندىلىرىنىلا كۆرسىتىدۇ ئۆرنەك + ئىشلىتىدىغان ئۆرنەكتىن بىرنى تاللاڭ diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index 4ea3a11c4..a9336a1fc 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -5,11 +5,17 @@ Не знайдено програм за запитом «%2$s». Нова версія підписана не тим ключем, що стара. Перш ніж встановити нову версію, самостійно зітріть стару. Зауважте, що стирання програми призведе до знищення всіх даних цієї програми. Версія + Наявно версій: %d + Наявна %d версія Зберігати звантажене + Зберігати звантажені APK-файли на карті пам’яті Оновлення Синхронізовано: %s ніколи + Синхронізація + Автоматично оновлювати список програм із репозиторію Сповіщення + Сповіщати про наявність оновлень Про F-Droid Сайт: Пошта: @@ -25,10 +31,11 @@ Назад Видалити репозиторій Оновити репозиторії? + Встановлене Наявне Оновлення Зачекайте - Оновлюю список програм… + Оновлюю список програм... Звантажую програму Адреса репозиторію Список репозиторіїв змінено. @@ -52,11 +59,15 @@ Отриманий файл пошкоджений Звантаження скасовано Експерт + Увімкнути режим експерта Пошук програм Синхронізація БД + Режим синхронізації SQLite Сумісність Суперкористувач + Показувати програми, для яких потрібні права суперкористувача Ігнорувати тачскрін + Завжди показувати програми, які потребують тачскрін Всі програми Недавні додання Недавні оновлення diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index 80451bdda..b7cd6f111 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -5,11 +5,17 @@ 没有找到 \'%s\'相关内容 新版本签名与旧版本不同,请先卸载旧版本应用再安装新版本。(注意:卸载旧版本会清除该应用的所有已储存数据) 版本 + %d个可用版本 + %d个可用版本 已下载应用缓存 + 在SD卡中保留下载的apk文件 升级 最后一次repo扫描: 从不 + 自动扫描repo + 自动更新应用列表 通知 + 当有更新时,通知栏提醒 关于F-Droid 网站: 邮件: @@ -25,6 +31,7 @@ 取消 选择要移除的应用源 更新应用源 + 已经安装的 可安装 更新 请等一下 @@ -52,11 +59,15 @@ 文件下载错误 下载取消 高级 + 开启高级模式 搜索应用 数据同步模式 + 设置 SQLite\'s synchronous flag的值 应用兼容性 Root + 显示需要root权限的应用 忽略需要触屏的应用 + 总是显示需要触屏的应用 全部 新鲜货 最近更新 From 4cd73c12f00278f741e46c002f600d8fdf791c6f Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 21 Jan 2014 16:56:41 -0500 Subject: [PATCH 031/282] add my recent changes to the changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b26e833f..6f5fab50c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,14 @@ * Fixed a crash when trying to access a non-existing app +* F-Droid registers with Android to receive F-Droid URIs https://*/fdroid/repo + and fdroidrepos:// + +* support including signing key fingerprint in repo URIs + +* when adding new repos that include the fingerprint, check to see whether + that repo exists in F-Droid already, and if the fingerprints match + * Other minor bug fixes * Lots of translation updates From 47659b5cecea6ae30794d0348976f1ab5cd0ad9f Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 21 Jan 2014 18:30:02 -0500 Subject: [PATCH 032/282] relabel misnamed items from "signature" to "repo fingerprint" In the new Repo Details screen, there is some elements that are labels as the "signature". This is not quite right, it is actually referring to the fingerprint of the repo signing key. Since a repo will also usually have a HTTPS certificate fingerprint, there will also be a fingerprint for that certificate. --- res/layout/repodetails.xml | 12 +++---- res/values/strings.xml | 2 +- .../views/fragments/RepoDetailsFragment.java | 36 +++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/res/layout/repodetails.xml b/res/layout/repodetails.xml index ad06cdc07..f7702e100 100644 --- a/res/layout/repodetails.xml +++ b/res/layout/repodetails.xml @@ -90,8 +90,8 @@ @@ -100,15 +100,15 @@ 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"/> + android:id="@+id/text_repo_fingerprint" + android:layout_below="@id/label_repo_fingerprint" android:textStyle="bold"/> + android:id="@+id/text_repo_fingerprint_description" + android:layout_below="@id/text_repo_fingerprint"/> Unsigned URL Number of apps - Signature + Fingerprint of Repo Signing Key (SHA1) Description Last update Update diff --git a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java index 84c3d347b..367aacb5a 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java @@ -32,9 +32,9 @@ public class RepoDetailsFragment extends Fragment { 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 + R.id.label_repo_fingerprint, + R.id.text_repo_fingerprint, + R.id.text_repo_fingerprint_description }; /** @@ -177,7 +177,7 @@ public class RepoDetailsFragment extends Fragment { numApps.setText(Integer.toString(repo.getNumberOfApps())); setupDescription(repoView, repo); - setupSignature(repoView, repo); + setupRepoFingerprint(repoView, repo); // Repos that existed before this feature was supported will have an // "Unknown" last update until next time they update... @@ -310,26 +310,26 @@ public class RepoDetailsFragment extends Fragment { ).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); + private void setupRepoFingerprint(ViewGroup parent, DB.Repo repo) { + TextView repoFingerprintView = (TextView)parent.findViewById(R.id.text_repo_fingerprint); + TextView repoFingerprintDescView = (TextView)parent.findViewById(R.id.text_repo_fingerprint_description); - String signature; - int signatureColour; + String repoFingerprint; + int repoFingerprintColor; if (repo.pubkey != null && repo.pubkey.length() > 0) { - signature = Utils.formatFingerprint(repo.pubkey); - signatureColour = getResources().getColor(R.color.signed); - signatureDescView.setVisibility(View.GONE); + repoFingerprint = Utils.formatFingerprint(repo.pubkey); + repoFingerprintColor = getResources().getColor(R.color.signed); + repoFingerprintDescView.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)); + repoFingerprint = getResources().getString(R.string.unsigned); + repoFingerprintColor = getResources().getColor(R.color.unsigned); + repoFingerprintDescView.setVisibility(View.VISIBLE); + repoFingerprintDescView.setText(getResources().getString(R.string.unsigned_description)); } - signatureView.setText(signature); - signatureView.setTextColor(signatureColour); + repoFingerprintView.setText(repoFingerprint); + repoFingerprintView.setTextColor(repoFingerprintColor); } public void onCreate(Bundle savedInstanceState) { From 526c978328cba09c3d2ca1d9829eed77eb1fedb6 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 21 Jan 2014 19:17:06 -0500 Subject: [PATCH 033/282] add ActionBarCompat.setTitle() method Now the title can be easily set in the ActionBar to reflect the current Activity. --- src/org/fdroid/fdroid/compat/ActionBarCompat.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/org/fdroid/fdroid/compat/ActionBarCompat.java b/src/org/fdroid/fdroid/compat/ActionBarCompat.java index e1c0f2cf1..b7c4450e7 100644 --- a/src/org/fdroid/fdroid/compat/ActionBarCompat.java +++ b/src/org/fdroid/fdroid/compat/ActionBarCompat.java @@ -21,6 +21,7 @@ public abstract class ActionBarCompat extends Compatibility { } public abstract void setDisplayHomeAsUpEnabled(boolean value); + public abstract void setTitle(CharSequence title); } @@ -34,6 +35,12 @@ class OldActionBarCompatImpl extends ActionBarCompat { public void setDisplayHomeAsUpEnabled(boolean value) { // Do nothing... } + + @Override + public void setTitle(CharSequence title) { + // Do nothing... + + } } @TargetApi(11) @@ -50,4 +57,9 @@ class HoneycombActionBarCompatImpl extends ActionBarCompat { public void setDisplayHomeAsUpEnabled(boolean value) { actionBar.setDisplayHomeAsUpEnabled(value); } + + @Override + public void setTitle(CharSequence title) { + actionBar.setTitle(title); + } } From 62008e85c6ce5b36cd6fdf4e8c3d9d39017c8047 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 21 Jan 2014 19:21:34 -0500 Subject: [PATCH 034/282] show repo name in ActionBar in "Repo Details" Before, "Repo Details" always had a title of "Repositories". Now it shows the name of the repo as the title. This also enables the ActionBar back button (aka "display home as up") to return to "Manage Repos". --- AndroidManifest.xml | 3 ++- .../fdroid/fdroid/views/RepoDetailsActivity.java | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 2a7bffbba..57f95bb7f 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -114,7 +114,8 @@ + android:label="@string/menu_manage" + android:parentActivityName=".ManageRepo" /> Date: Tue, 21 Jan 2014 20:54:59 -0500 Subject: [PATCH 035/282] reorganize update process to set up updating individual repos This keeps the same logic as was in place before, but splits out code from onHandleIntent() so that it can be used to update specific repos, rather than always updating all repos. This is needed for transient repos, like p2p repos connected via bluetooth or local wifi. --- src/org/fdroid/fdroid/UpdateService.java | 340 ++++++++++++----------- 1 file changed, 174 insertions(+), 166 deletions(-) diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index d81b2fddd..f1c2544ce 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -21,22 +21,32 @@ package org.fdroid.fdroid; import java.util.ArrayList; import java.util.List; -import android.app.*; +import android.app.AlarmManager; +import android.app.IntentService; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.ProgressDialog; 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.*; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.os.ResultReceiver; +import android.os.SystemClock; 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.text.TextUtils; +import android.util.Log; import android.widget.Toast; +import org.fdroid.fdroid.updater.RepoUpdater; + public class UpdateService extends IntentService implements ProgressListener { public static final String RESULT_MESSAGE = "msg"; @@ -232,177 +242,20 @@ public class UpdateService extends IntentService implements ProgressListener { } else { Log.d("FDroid", "Unscheduled (manually requested) update"); } - - boolean notify = prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, false); - - // Grab some preliminary information, then we can release the - // database while we do all the downloading, etc... - int updates = 0; - List repos; - List apps; - try { - DB db = DB.getDB(); - repos = db.getRepos(); - apps = db.getApps(false); - } finally { - DB.releaseDB(); - } - - // Process each repo... - List updatingApps = new ArrayList(); - List keeprepos = new ArrayList(); - boolean success = true; - boolean changes = false; - for (DB.Repo repo : repos) { - if (!repo.inuse) { - continue; - } - sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address)); - RepoUpdater updater = RepoUpdater.createUpdaterFor(getBaseContext(), repo); - updater.setProgressListener(this); - try { - updater.update(); - if (updater.hasChanged()) { - updatingApps.addAll(updater.getApps()); - changes = true; - } else { - keeprepos.add(repo.id); - } - } catch (RepoUpdater.UpdateException e) { - errmsg += (errmsg.length() == 0 ? "" : "\n") + e.getMessage(); - Log.e("FDroid", "Error updating repository " + repo.address + ": " + e.getMessage()); - Log.e("FDroid", Log.getStackTraceString(e)); - } - } - - if (!changes && success) { - Log.d("FDroid", - "Not checking app details or compatibility, " + - "because all repos were up to date."); - } else if (changes && success) { - - sendStatus(STATUS_INFO, - getString(R.string.status_checking_compatibility)); - - DB db = DB.getDB(); - try { - - // Need to flag things we're keeping despite having received - // no data about during the update. (i.e. stuff from a repo - // that we know is unchanged due to the etag) - for (int keep : keeprepos) { - for (DB.App app : apps) { - boolean keepapp = false; - for (DB.Apk apk : app.apks) { - if (apk.repo == keep) { - keepapp = true; - break; - } - } - if (keepapp) { - DB.App app_k = null; - for (DB.App app2 : apps) { - if (app2.id.equals(app.id)) { - app_k = app2; - break; - } - } - if (app_k == null) { - updatingApps.add(app); - app_k = app; - } - app_k.updated = true; - db.populateDetails(app_k, keep); - for (DB.Apk apk : app.apks) - if (apk.repo == keep) - apk.updated = true; - } - } - } - - db.beginUpdate(apps); - for (DB.App app : updatingApps) { - db.updateApplication(app); - } - db.endUpdate(); - for (DB.Repo repo : repos) - db.writeLastEtag(repo); - } catch (Exception ex) { - db.cancelUpdate(); - Log.e("FDroid", "Exception during update processing:\n" - + Log.getStackTraceString(ex)); - errmsg = "Exception during processing - " + ex.getMessage(); - success = false; - } finally { - DB.releaseDB(); - } - - } - - if (success && changes) { - ((FDroidApp) getApplication()).invalidateAllApps(); - if (notify) { - apps = ((FDroidApp) getApplication()).getApps(); - updates = getNumUpdates(apps); - } - } - - if (success && changes && notify && updates > 0) { - Log.d("FDroid", "Notifying "+updates+" updates."); - NotificationCompat.Builder mBuilder = - new NotificationCompat.Builder( - this) - .setAutoCancel(true) - .setContentTitle( - getString(R.string.fdroid_updates_available)); - if (Build.VERSION.SDK_INT >= 11) { - mBuilder.setSmallIcon(R.drawable.ic_stat_notify_updates); - } else { - mBuilder.setSmallIcon(R.drawable.ic_launcher); - } - Intent notifyIntent = new Intent(this, FDroid.class) - .putExtra(FDroid.EXTRA_TAB_UPDATE, true); - if (updates > 1) { - mBuilder.setContentText(getString( - R.string.many_updates_available, updates)); - - } else { - mBuilder.setContentText(getString(R.string.one_update_available)); - } - TaskStackBuilder stackBuilder = TaskStackBuilder - .create(this).addParentStack(FDroid.class) - .addNextIntent(notifyIntent); - PendingIntent pendingIntent = stackBuilder - .getPendingIntent(0, - PendingIntent.FLAG_UPDATE_CURRENT); - mBuilder.setContentIntent(pendingIntent); - NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - mNotificationManager.notify(1, mBuilder.build()); - } - - if (!success) { - if (errmsg.length() == 0) - errmsg = "Unknown error"; - sendStatus(STATUS_ERROR, errmsg); - } else { + errmsg = updateRepos(); + if (TextUtils.isEmpty(errmsg)) { Editor e = prefs.edit(); e.putLong(Preferences.PREF_UPD_LAST, System.currentTimeMillis()); e.commit(); - if (changes) { - sendStatus(STATUS_COMPLETE_WITH_CHANGES); - } else { - sendStatus(STATUS_COMPLETE_AND_SAME); - } } - } catch (Exception e) { Log.e("FDroid", "Exception during update processing:\n" + Log.getStackTraceString(e)); - if (errmsg.length() == 0) + if (TextUtils.isEmpty(errmsg)) errmsg = "Unknown error"; sendStatus(STATUS_ERROR, errmsg); - } finally { + } finally { Log.d("FDroid", "Update took " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds."); @@ -410,6 +263,161 @@ public class UpdateService extends IntentService implements ProgressListener { } } + protected String updateRepos() throws Exception { + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(getBaseContext()); + boolean notify = prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, false); + String errmsg = ""; + // Grab some preliminary information, then we can release the + // database while we do all the downloading, etc... + int updates = 0; + List repos; + List apps; + try { + DB db = DB.getDB(); + repos = db.getRepos(); + apps = db.getApps(false); + } finally { + DB.releaseDB(); + } + + // Process each repo... + List updatingApps = new ArrayList(); + List keeprepos = new ArrayList(); + boolean changes = false; + for (DB.Repo repo : repos) { + if (!repo.inuse) + continue; + sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address)); + RepoUpdater updater = RepoUpdater.createUpdaterFor(getBaseContext(), repo); + updater.setProgressListener(this); + try { + updater.update(); + if (updater.hasChanged()) { + updatingApps.addAll(updater.getApps()); + changes = true; + } else { + keeprepos.add(repo.id); + } + } catch (RepoUpdater.UpdateException e) { + errmsg += (errmsg.length() == 0 ? "" : "\n") + e.getMessage(); + Log.e("FDroid", "Error updating repository " + repo.address + ": " + e.getMessage()); + Log.e("FDroid", Log.getStackTraceString(e)); + } + } + + boolean success = true; + if (!changes) { + Log.d("FDroid", "Not checking app details or compatibility, " + + "because all repos were up to date."); + } else { + sendStatus(STATUS_INFO, getString(R.string.status_checking_compatibility)); + + DB db = DB.getDB(); + try { + + // Need to flag things we're keeping despite having received + // no data about during the update. (i.e. stuff from a repo + // that we know is unchanged due to the etag) + for (int keep : keeprepos) { + for (DB.App app : apps) { + boolean keepapp = false; + for (DB.Apk apk : app.apks) { + if (apk.repo == keep) { + keepapp = true; + break; + } + } + if (keepapp) { + DB.App app_k = null; + for (DB.App app2 : apps) { + if (app2.id.equals(app.id)) { + app_k = app2; + break; + } + } + if (app_k == null) { + updatingApps.add(app); + app_k = app; + } + app_k.updated = true; + db.populateDetails(app_k, keep); + for (DB.Apk apk : app.apks) + if (apk.repo == keep) + apk.updated = true; + } + } + } + + db.beginUpdate(apps); + for (DB.App app : updatingApps) { + db.updateApplication(app); + } + db.endUpdate(); + for (DB.Repo repo : repos) + db.writeLastEtag(repo); + } catch (Exception ex) { + db.cancelUpdate(); + Log.e("FDroid", "Exception during update processing:\n" + + Log.getStackTraceString(ex)); + errmsg = "Exception during processing - " + ex.getMessage(); + success = false; + } finally { + DB.releaseDB(); + } + } + + if (success && changes) { + ((FDroidApp) getApplication()).invalidateAllApps(); + if (notify) { + apps = ((FDroidApp) getApplication()).getApps(); + updates = getNumUpdates(apps); + } + if (notify && updates > 0) + showAppUpdatesNotification(updates); + } + + if (success) { + if (changes) { + sendStatus(STATUS_COMPLETE_WITH_CHANGES); + } else { + sendStatus(STATUS_COMPLETE_AND_SAME); + } + } else { + if (TextUtils.isEmpty(errmsg)) + errmsg = "Unknown error"; + sendStatus(STATUS_ERROR, errmsg); + } + + return errmsg; + } + + private void showAppUpdatesNotification(int updates) throws Exception { + Log.d("FDroid", "Notifying " + updates + " updates."); + NotificationCompat.Builder builder = + new NotificationCompat.Builder(this) + .setAutoCancel(true) + .setContentTitle(getString(R.string.fdroid_updates_available)); + if (Build.VERSION.SDK_INT >= 11) { + builder.setSmallIcon(R.drawable.ic_stat_notify_updates); + } else { + builder.setSmallIcon(R.drawable.ic_launcher); + } + Intent notifyIntent = new Intent(this, FDroid.class) + .putExtra(FDroid.EXTRA_TAB_UPDATE, true); + if (updates > 1) { + builder.setContentText(getString(R.string.many_updates_available, updates)); + } else { + builder.setContentText(getString(R.string.one_update_available)); + } + TaskStackBuilder stackBuilder = TaskStackBuilder + .create(this).addParentStack(FDroid.class) + .addNextIntent(notifyIntent); + PendingIntent pi = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + builder.setContentIntent(pi); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(1, builder.build()); + } /** * Received progress event from the RepoXMLHandler. It could be progress From cbb182c18a311aad4548ace1a6793d0f078fa875 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 21 Jan 2014 21:52:56 -0500 Subject: [PATCH 036/282] add method for updating a single repo while leaving the rest as is Now, clicking Update in "Repo Detail" only updates that repo, not all of the repos. This will also be very useful for more transitory repos, like p2p repos that are reached via bluetooth, local wifi, etc. --- src/org/fdroid/fdroid/UpdateService.java | 30 +++++++++++++++++-- .../views/fragments/RepoDetailsFragment.java | 2 +- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index f1c2544ce..f2c13b760 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -20,6 +20,8 @@ package org.fdroid.fdroid; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.TreeSet; import android.app.AlarmManager; import android.app.IntentService; @@ -119,6 +121,10 @@ public class UpdateService extends IntentService implements ProgressListener { } public static UpdateReceiver updateNow(Context context) { + return updateRepoNow(null, context); + } + + public static UpdateReceiver updateRepoNow(String address, 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); @@ -129,6 +135,8 @@ public class UpdateService extends IntentService implements ProgressListener { UpdateReceiver receiver = new UpdateReceiver(new Handler()); receiver.setContext(context).setDialog(dialog); intent.putExtra("receiver", receiver); + if (!TextUtils.isEmpty(address)) + intent.putExtra("address", address); context.startService(intent); return receiver; @@ -204,6 +212,7 @@ public class UpdateService extends IntentService implements ProgressListener { protected void onHandleIntent(Intent intent) { receiver = intent.getParcelableExtra("receiver"); + String address = intent.getStringExtra("address"); long startTime = System.currentTimeMillis(); String errmsg = ""; @@ -242,7 +251,7 @@ public class UpdateService extends IntentService implements ProgressListener { } else { Log.d("FDroid", "Unscheduled (manually requested) update"); } - errmsg = updateRepos(); + errmsg = updateRepos(address); if (TextUtils.isEmpty(errmsg)) { Editor e = prefs.edit(); e.putLong(Preferences.PREF_UPD_LAST, System.currentTimeMillis()); @@ -263,7 +272,7 @@ public class UpdateService extends IntentService implements ProgressListener { } } - protected String updateRepos() throws Exception { + protected String updateRepos(String address) throws Exception { SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(getBaseContext()); boolean notify = prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, false); @@ -283,11 +292,26 @@ public class UpdateService extends IntentService implements ProgressListener { // Process each repo... List updatingApps = new ArrayList(); - List keeprepos = new ArrayList(); + Set keeprepos = new TreeSet(); boolean changes = false; + boolean update; for (DB.Repo repo : repos) { if (!repo.inuse) continue; + // are we updating all repos, or just one? + if (TextUtils.isEmpty(address)) { + update = true; + } else { + // if only updating one repo, mark the rest as keepers + if (address.equals(repo.address)) { + update = true; + } else { + keeprepos.add(repo.id); + update = false; + } + } + if (!update) + continue; sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address)); RepoUpdater updater = RepoUpdater.createUpdaterFor(getBaseContext(), repo); updater.setProgressListener(this); diff --git a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java index 367aacb5a..96d2b3d45 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java @@ -211,7 +211,7 @@ public class RepoDetailsFragment extends Fragment { */ private void performUpdate() { repo.enable((FDroidApp)getActivity().getApplication()); - UpdateService.updateNow(getActivity()).setListener(new ProgressListener() { + UpdateService.updateRepoNow(repo.address, getActivity()).setListener(new ProgressListener() { @Override public void onProgress(Event event) { if (event.type == UpdateService.STATUS_COMPLETE_AND_SAME || From cad37b0d554fefb8b2f1f0a9bf022770f0ff5eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 22 Jan 2014 10:05:49 +0100 Subject: [PATCH 037/282] Revert AB.setTitle, support titles before 3.0 too --- src/org/fdroid/fdroid/compat/ActionBarCompat.java | 10 ---------- src/org/fdroid/fdroid/views/RepoDetailsActivity.java | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/org/fdroid/fdroid/compat/ActionBarCompat.java b/src/org/fdroid/fdroid/compat/ActionBarCompat.java index b7c4450e7..52e077673 100644 --- a/src/org/fdroid/fdroid/compat/ActionBarCompat.java +++ b/src/org/fdroid/fdroid/compat/ActionBarCompat.java @@ -21,7 +21,6 @@ public abstract class ActionBarCompat extends Compatibility { } public abstract void setDisplayHomeAsUpEnabled(boolean value); - public abstract void setTitle(CharSequence title); } @@ -36,11 +35,6 @@ class OldActionBarCompatImpl extends ActionBarCompat { // Do nothing... } - @Override - public void setTitle(CharSequence title) { - // Do nothing... - - } } @TargetApi(11) @@ -58,8 +52,4 @@ class HoneycombActionBarCompatImpl extends ActionBarCompat { actionBar.setDisplayHomeAsUpEnabled(value); } - @Override - public void setTitle(CharSequence title) { - actionBar.setTitle(title); - } } diff --git a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java index 087e753c5..edf438d53 100644 --- a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -42,7 +42,7 @@ public class RepoDetailsActivity extends FragmentActivity implements RepoDetails ActionBarCompat abCompat = ActionBarCompat.create(this); abCompat.setDisplayHomeAsUpEnabled(true); - abCompat.setTitle(repo.getName()); + setTitle(repo.getName()); } private void finishWithAction(String actionName) { From f8893431fb78b154196f151349dd113025034b5c Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 23 Jan 2014 12:27:38 +1100 Subject: [PATCH 038/282] Refactored Repo db access from DB class to ContentProvider. The performance improvement from this will not be noticable (perhaps there isn't one), however it is part of the bigger plan to move all of the DB access to ContentProviders. This will make a big improvement to the startup time of the app, given we are currently loading all of the apps to populate the list of apps. It will come at the cost of some apparantly weird code convensions. Most notably, when loading data from a content provder, you only ask for the fields that you intend to use. As a result of my Helper class which converts results from the content providers cursor into Repo value objects, there is no guarantee that certain attributes will be available on the value object. E.g. if I load repos and only ask for "_ID" and "ADDRESS", then it is meaningless to ask the resulting Repo object for its "VERSION" (it wont be there), despite it being a perfectly legal attribute from the Java compilers perspective. Repo.id field has also been made private (sqlite is the only entity should be able to set id's), and made id a long (sqlite stores identifiers as longs rather than ints). --- AndroidManifest.xml | 6 + src/org/fdroid/fdroid/AppDetails.java | 21 +- src/org/fdroid/fdroid/DB.java | 372 ++---------------- src/org/fdroid/fdroid/ManageRepo.java | 347 ++++++++-------- src/org/fdroid/fdroid/RepoXMLHandler.java | 13 +- src/org/fdroid/fdroid/UpdateService.java | 21 +- src/org/fdroid/fdroid/Utils.java | 3 +- .../fdroid/fdroid/compat/SwitchCompat.java | 27 +- src/org/fdroid/fdroid/data/DBHelper.java | 139 +++++-- .../fdroid/fdroid/data/FDroidProvider.java | 59 +++ src/org/fdroid/fdroid/data/Repo.java | 176 +++++++++ src/org/fdroid/fdroid/data/RepoProvider.java | 294 ++++++++++++++ .../fdroid/fdroid/updater/RepoUpdater.java | 54 ++- .../fdroid/updater/SignedRepoUpdater.java | 3 +- .../fdroid/updater/UnsignedRepoUpdater.java | 3 +- src/org/fdroid/fdroid/views/RepoAdapter.java | 113 +++--- .../fdroid/views/RepoDetailsActivity.java | 64 +-- .../views/fragments/RepoDetailsFragment.java | 106 ++--- 18 files changed, 1023 insertions(+), 798 deletions(-) create mode 100644 src/org/fdroid/fdroid/data/FDroidProvider.java create mode 100644 src/org/fdroid/fdroid/data/Repo.java create mode 100644 src/org/fdroid/fdroid/data/RepoProvider.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 57f95bb7f..89646a26b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -33,6 +33,12 @@ android:allowBackup="true" android:theme="@style/AppThemeDark" android:supportsRtl="false" > + + + diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index a63b4bb29..c01eccdb5 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -25,6 +25,8 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; import org.xml.sax.XMLReader; import android.app.AlertDialog; @@ -859,20 +861,13 @@ public class AppDetails extends ListActivity { // Install the version of this app denoted by 'app.curApk'. private void install() { - String ra = null; - try { - DB db = DB.getDB(); - DB.Repo repo = db.getRepo(app.curApk.repo); - if (repo != null) - ra = repo.address; - } catch (Exception ex) { - Log.d("FDroid", "Failed to get repo address - " + ex.getMessage()); - } finally { - DB.releaseDB(); - } - if (ra == null) + String [] projection = { RepoProvider.DataColumns.ADDRESS }; + Repo repo = RepoProvider.Helper.findById( + getContentResolver(), app.curApk.repo, projection); + if (repo == null || repo.address == null) { return; - final String repoaddress = ra; + } + final String repoaddress = repo.address; if (!app.curApk.compatible) { AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this); diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index 2ba046f4b..73099ee64 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -55,6 +55,7 @@ import org.fdroid.fdroid.compat.Compatibility; import org.fdroid.fdroid.compat.ContextCompat; import org.fdroid.fdroid.compat.SupportedArchitectures; import org.fdroid.fdroid.data.DBHelper; +import org.fdroid.fdroid.data.Repo; public class DB { @@ -290,7 +291,7 @@ public class DB { public String version; public int vercode; public int detail_size; // Size in bytes - 0 means we don't know! - public int repo; // ID of the repo it comes from + public long repo; // ID of the repo it comes from public String detail_hash; public String detail_hashType; public int minSdkVersion; // 0 if unknown @@ -404,111 +405,9 @@ public class DB { } } - // The TABLE_REPO table stores the details of the repositories in use. - public static final String TABLE_REPO = "fdroid_repo"; - - public static class Repo { - public int id; - public String address; - public String name; - public String description; - public int version; // index version, i.e. what fdroidserver built it - 0 if not specified - public boolean inuse; - public int priority; - public String pubkey; // null for an unsigned repo - 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 toEnable = new ArrayList(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 toDisable = new ArrayList(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 int countAppsForRepo(int id) { + public int countAppsForRepo(long id) { String[] selection = { "COUNT(distinct id)" }; - String[] selectionArgs = { Integer.toString(id) }; + String[] selectionArgs = { Long.toString(id) }; Cursor result = db.query( TABLE_APK, selection, "repo = ?", selectionArgs, "repo", null, null); if (result.getCount() > 0) { @@ -554,8 +453,7 @@ public class DB { // The date format used for storing dates (e.g. lastupdated, added) in the // database. - public static SimpleDateFormat dateFormat = new SimpleDateFormat( - "yyyy-MM-dd", Locale.ENGLISH); + public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); private DB(Context ctx) { @@ -659,7 +557,7 @@ public class DB { private static final String[] POPULATE_APK_COLS = new String[] { "hash", "hashType", "size", "permissions" }; - private void populateApkDetails(Apk apk, int repo) { + private void populateApkDetails(Apk apk, long repo) { if (repo == 0 || repo == apk.repo) { Cursor cursor = null; try { @@ -692,7 +590,7 @@ public class DB { // Populate the details for the given app, if necessary. // If 'apkrepo' is non-zero, only apks from that repo are // populated (this is used during the update process) - public void populateDetails(App app, int apkRepo) { + public void populateDetails(App app, long apkRepo) { if (!app.detail_Populated) { populateAppDetails(app); } @@ -747,10 +645,10 @@ public class DB { app.curVercode = c.getInt(9); String sAdded = c.getString(10); app.added = (sAdded == null || sAdded.length() == 0) ? null - : dateFormat.parse(sAdded); + : DATE_FORMAT.parse(sAdded); String sLastUpdated = c.getString(11); app.lastUpdated = (sLastUpdated == null || sLastUpdated - .length() == 0) ? null : dateFormat + .length() == 0) ? null : DATE_FORMAT .parse(sLastUpdated); app.compatible = c.getInt(12) == 1; app.ignoreAllUpdates = c.getInt(13) == 1; @@ -783,12 +681,16 @@ public class DB { Log.d("FDroid", "Read app data from database " + " (took " + (System.currentTimeMillis() - startTime) + " ms)"); - List repos = getRepos(); - cols = new String[] { "id", "version", "vercode", "sig", "srcname", - "apkName", "minSdkVersion", "added", "features", "nativecode", - "compatible", "repo" }; - c = db.query(TABLE_APK, cols, null, null, null, null, - "vercode desc"); + String query = "SELECT apk.id, apk.version, apk.vercode, apk.sig," + + " apk.srcname, apk.apkName, apk.minSdkVersion, " + + " apk.added, apk.features, apk.nativecode, " + + " apk.compatible, apk.repo, repo.version, repo.address " + + " FROM " + TABLE_APK + " as apk " + + " LEFT JOIN " + DBHelper.TABLE_REPO + " as repo " + + " ON repo._id = apk.repo " + + " ORDER BY apk.vercode DESC"; + + c = db.rawQuery(query, null); c.moveToFirst(); DisplayMetrics metrics = mContext.getResources() @@ -829,21 +731,19 @@ public class DB { apk.minSdkVersion = c.getInt(6); String sApkAdded = c.getString(7); apk.added = (sApkAdded == null || sApkAdded.length() == 0) ? null - : dateFormat.parse(sApkAdded); + : DATE_FORMAT.parse(sApkAdded); apk.features = CommaSeparatedList.make(c.getString(8)); apk.nativecode = CommaSeparatedList.make(c.getString(9)); apk.compatible = compatible; apk.repo = repoid; app.apks.add(apk); if (app.iconUrl == null && app.icon != null) { - for (DB.Repo repo : repos) { - if (repo.id != repoid) continue; - if (repo.version >= 11) { - app.iconUrl = repo.address + iconsDir + app.icon; - } else { - app.iconUrl = repo.address + "/icons/" + app.icon; - } - break; + int repoVersion = c.getInt(12); + String repoAddress = c.getString(13); + if (repoVersion >= 11) { + app.iconUrl = repoAddress + iconsDir + app.icon; + } else { + app.iconUrl = repoAddress + "/icons/" + app.icon; } } c.moveToNext(); @@ -1153,10 +1053,10 @@ public class DB { values.put("dogecoinAddr", upapp.detail_dogecoinAddr); values.put("flattrID", upapp.detail_flattrID); values.put("added", - upapp.added == null ? "" : dateFormat.format(upapp.added)); + upapp.added == null ? "" : DATE_FORMAT.format(upapp.added)); values.put( "lastUpdated", - upapp.added == null ? "" : dateFormat + upapp.added == null ? "" : DATE_FORMAT .format(upapp.lastUpdated)); values.put("curVersion", upapp.curVersion); values.put("curVercode", upapp.curVercode); @@ -1201,7 +1101,7 @@ public class DB { values.put("apkName", upapk.apkName); values.put("minSdkVersion", upapk.minSdkVersion); values.put("added", - upapk.added == null ? "" : dateFormat.format(upapk.added)); + upapk.added == null ? "" : DATE_FORMAT.format(upapk.added)); values.put("permissions", CommaSeparatedList.str(upapk.detail_permissions)); values.put("features", CommaSeparatedList.str(upapk.features)); @@ -1216,105 +1116,6 @@ public class DB { } } - // Get details of a repo, given the ID. Returns null if the repo - // doesn't exist. - public Repo getRepo(int id) { - Cursor c = null; - try { - c = db.query(TABLE_REPO, new String[] { "address", "name", - "description", "version", "inuse", "priority", "pubkey", - "fingerprint", "maxage", "lastetag", "lastUpdated" }, - "id = ?", new String[] { Integer.toString(id) }, null, null, null); - if (!c.moveToFirst()) - return null; - Repo repo = new Repo(); - repo.id = id; - repo.address = c.getString(0); - repo.name = c.getString(1); - repo.description = c.getString(2); - repo.version = c.getInt(3); - repo.inuse = (c.getInt(4) == 1); - repo.priority = c.getInt(5); - repo.pubkey = c.getString(6); - repo.fingerprint = c.getString(7); - repo.maxage = c.getInt(8); - repo.lastetag = c.getString(9); - try { - repo.lastUpdated = c.getString(10) != null ? - dateFormat.parse( c.getString(10)) : - null; - } catch (ParseException e) { - Log.e("FDroid", "Error parsing date " + c.getString(10)); - } - return repo; - } finally { - if (c != null) - c.close(); - } - } - - // Get a list of the configured repositories. - public List getRepos() { - List repos = new ArrayList(); - Cursor c = null; - try { - c = db.query(TABLE_REPO, new String[] { "id", "address", "name", - "description", "version", "inuse", "priority", "pubkey", - "fingerprint", "maxage", "lastetag" }, - null, null, null, null, "priority"); - c.moveToFirst(); - while (!c.isAfterLast()) { - Repo repo = new Repo(); - repo.id = c.getInt(0); - repo.address = c.getString(1); - repo.name = c.getString(2); - repo.description = c.getString(3); - repo.version = c.getInt(4); - repo.inuse = (c.getInt(5) == 1); - repo.priority = c.getInt(6); - repo.pubkey = c.getString(7); - repo.fingerprint = c.getString(8); - repo.maxage = c.getInt(9); - repo.lastetag = c.getString(10); - repos.add(repo); - c.moveToNext(); - } - } catch (Exception e) { - } finally { - if (c != null) { - c.close(); - } - } - return repos; - } - - public void enableRepos(List 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 = ?", - new String[] { address }); - } - public void setIgnoreUpdates(String appid, boolean All, int This) { db.execSQL("update " + TABLE_APP + " set" + " ignoreAllUpdates=" + (All ? '1' : '0') @@ -1322,120 +1123,11 @@ public class DB { + " where id = ?", new String[] { appid }); } - 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); - values.put("priority", repo.priority); - values.put("pubkey", repo.pubkey); - if (repo.pubkey != null && repo.fingerprint == null) { - // we got a new pubkey, so calc the fingerprint - values.put("fingerprint", DB.calcFingerprint(repo.pubkey)); - } else { - values.put("fingerprint", repo.fingerprint); - } - values.put("maxage", repo.maxage); - values.put("lastetag", (String) null); - 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", dateFormat.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", dateFormat.format(new Date())); - db.update(TABLE_REPO, values, "address = ?", - new String[] { repo.address }); - } - - public void addRepo(String address, String name, String description, - int version, int priority, String pubkey, String fingerprint, - int maxage, boolean inuse) - throws SecurityException { - ContentValues values = new ContentValues(); - values.put("address", address); - values.put("name", name); - values.put("description", description); - values.put("version", version); - values.put("inuse", inuse ? 1 : 0); - values.put("priority", priority); - values.put("pubkey", pubkey); - String calcedFingerprint = DB.calcFingerprint(pubkey); - if (fingerprint == null) { - fingerprint = calcedFingerprint; - } else if (calcedFingerprint != null) { - fingerprint = fingerprint.toUpperCase(Locale.ENGLISH); - if (!fingerprint.equals(calcedFingerprint)) { - throw new SecurityException("Given fingerprint does not match calculated one! (" - + fingerprint + " != " + calcedFingerprint); - } - } - values.put("fingerprint", fingerprint); - values.put("maxage", maxage); - values.put("lastetag", (String) null); - db.insert(TABLE_REPO, null, values); - } - - public void doDisableRepos(List repos, boolean remove) { - if (repos.isEmpty()) return; + public void purgeApps(Repo repo, FDroidApp fdroid) { db.beginTransaction(); - // 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}, - null, null, null, null); - c.moveToFirst(); - if (!c.isAfterLast()) { - db.delete(TABLE_APK, "repo = ?", - new String[] { Integer.toString(c.getInt(0)) }); - } - } finally { - if (c != null) { - c.close(); - } - } - 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 }); - } - } + db.delete(TABLE_APK, "repo = ?", new String[] { Long.toString(repo.getId()) }); List apps = getApps(false); for (App app : apps) { if (app.apks.isEmpty()) { @@ -1446,6 +1138,8 @@ public class DB { } finally { db.endTransaction(); } + + fdroid.invalidateAllApps(); } public int getSynchronizationMode() { diff --git a/src/org/fdroid/fdroid/ManageRepo.java b/src/org/fdroid/fdroid/ManageRepo.java index 50191ff71..765999a76 100644 --- a/src/org/fdroid/fdroid/ManageRepo.java +++ b/src/org/fdroid/fdroid/ManageRepo.java @@ -21,34 +21,39 @@ package org.fdroid.fdroid; import android.app.Activity; import android.app.AlertDialog; -import android.app.ListActivity; +import android.content.ContentValues; import android.content.DialogInterface; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.ListFragment; +import android.support.v4.content.CursorLoader; import android.content.Intent; +import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.support.v4.app.LoaderManager; import android.support.v4.app.NavUtils; +import android.support.v4.content.Loader; import android.support.v4.view.MenuItemCompat; import android.util.Log; import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; 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.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; 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.*; +import java.util.Locale; -public class ManageRepo extends ListActivity { - - private static final String DEFAULT_NEW_REPO_TEXT = "https://"; - private final int ADD_REPO = 1; - private final int UPDATE_REPOS = 2; +public class ManageRepo extends FragmentActivity { /** * If we have a new repo added, or the address of a repo has changed, then @@ -58,6 +63,122 @@ public class ManageRepo extends ListActivity { */ public static final String REQUEST_UPDATE = "update"; + private RepoListFragment listFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ((FDroidApp) getApplication()).applyTheme(this); + + if (savedInstanceState == null) { + listFragment = new RepoListFragment(); + getSupportFragmentManager() + .beginTransaction() + .add(android.R.id.content, listFragment) + .commit(); + } + + ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); + } + + public void finish() { + Intent ret = new Intent(); + if (listFragment.hasChanged()) { + Log.i("FDroid", "Repo details have changed, prompting for update."); + ret.putExtra(REQUEST_UPDATE, true); + } + setResult(Activity.RESULT_OK, ret); + super.finish(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + } + return super.onOptionsItemSelected(item); + } + +} + +class RepoListFragment extends ListFragment + implements LoaderManager.LoaderCallbacks,RepoAdapter.EnabledListener { + + private static final String DEFAULT_NEW_REPO_TEXT = "https://"; + private final int ADD_REPO = 1; + private final int UPDATE_REPOS = 2; + + public boolean hasChanged() { + return changed; + } + + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + Uri uri = RepoProvider.getContentUri(); + Log.i("FDroid", "Creating repo loader '" + uri + "'."); + String[] projection = new String[] { + RepoProvider.DataColumns._ID, + RepoProvider.DataColumns.NAME, + RepoProvider.DataColumns.PUBLIC_KEY, + RepoProvider.DataColumns.FINGERPRINT, + RepoProvider.DataColumns.IN_USE + }; + return new CursorLoader(getActivity(), uri, projection, null, null, null); + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + Log.i("FDroid", "Repo cursor loaded."); + repoAdapter.swapCursor(cursor); + } + + @Override + public void onLoaderReset(Loader cursorLoader) { + Log.i("FDroid", "Repo cursor reset."); + repoAdapter.swapCursor(null); + } + + + /** + * 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. + */ + @Override + public void onSetEnabled(Repo repo, boolean isEnabled) { + if (repo.inuse != isEnabled ) { + ContentValues values = new ContentValues(1); + values.put(RepoProvider.DataColumns.IN_USE, isEnabled ? 1 : 0); + RepoProvider.Helper.update( + getActivity().getContentResolver(), repo, values); + + if (isEnabled) { + changed = true; + } else { + FDroidApp app = (FDroidApp)getActivity().getApplication(); + RepoProvider.Helper.purgeApps(repo, app); + String notification = getString(R.string.repo_disabled_notification, repo.name); + Toast.makeText(getActivity(), notification, Toast.LENGTH_LONG).show(); + } + } + } + private enum PositiveAction { ADD_NEW, ENABLE, IGNORE } @@ -74,16 +195,14 @@ public class ManageRepo extends ListActivity { private boolean isImportingRepo = false; @Override - protected void onCreate(Bundle savedInstanceState) { - - ((FDroidApp) getApplication()).applyTheme(this); + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ActionBarCompat abCompat = ActionBarCompat.create(this); - abCompat.setDisplayHomeAsUpEnabled(true); + setHasOptionsMenu(true); - repoAdapter = new RepoAdapter(this); + repoAdapter = new RepoAdapter(getActivity(), null); + repoAdapter.setEnabledListener(this); setListAdapter(repoAdapter); /* @@ -106,7 +225,7 @@ public class ManageRepo extends ListActivity { */ /* let's see if someone is trying to send us a new repo */ - Intent intent = getIntent(); + Intent intent = getActivity().getIntent(); /* an URL from a click or a QRCode scan */ Uri uri = intent.getData(); if (uri != null) { @@ -139,27 +258,25 @@ public class ManageRepo extends ListActivity { } @Override - protected void onResume() { + public void onResume() { super.onResume(); - refreshList(); + + //Starts a new or restarts an existing Loader in this manager + getLoaderManager().restartLoader(0, null, this); } @Override - protected void onListItemClick(ListView l, View v, int position, long id) { + public void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); - DB.Repo repo = (DB.Repo)getListView().getItemAtPosition(position); + Repo repo = new Repo((Cursor)getListView().getItemAtPosition(position)); editRepo(repo); } - private void refreshList() { - repoAdapter.refresh(); - } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); MenuItem updateItem = menu.add(Menu.NONE, UPDATE_REPOS, 1, R.string.menu_update_repo).setIcon(R.drawable.ic_menu_refresh); @@ -171,102 +288,23 @@ public class ManageRepo extends ListActivity { android.R.drawable.ic_menu_add); MenuItemCompat.setShowAsAction(addItem, MenuItemCompat.SHOW_AS_ACTION_ALWAYS | - MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT); - - return true; + MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT); } 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); + public void editRepo(Repo repo) { + Intent intent = new Intent(getActivity(), RepoDetailsActivity.class); + intent.putExtra(RepoDetailsFragment.ARG_REPO_ID, repo.getId()); 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 reposToRemove = new ArrayList(1); - reposToRemove.add(repo); - try { - DB db = DB.getDB(); - db.doDisableRepos(reposToRemove, true); - } finally { - DB.releaseDB(); - } - refreshList(); - } - - protected List getRepos() { - List repos = null; - try { - DB db = DB.getDB(); - repos = db.getRepos(); - } finally { - DB.releaseDB(); - } - return repos; - } - - protected Repo getRepoByAddress(String address, List repos) { - if (address != null) - for (Repo repo : repos) - if (address.equals(repo.address)) - return repo; - return null; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; - } - return super.onOptionsItemSelected(item); - } - private void updateRepos() { - UpdateService.updateNow(this).setListener(new ProgressListener() { + UpdateService.updateNow(getActivity()).setListener(new ProgressListener() { @Override public void onProgress(Event event) { // No need to prompt to update any more, we just did it! changed = false; - refreshList(); } }); } @@ -276,14 +314,14 @@ public class ManageRepo extends ListActivity { } private void showAddRepo(String newAddress, String newFingerprint) { - - View view = getLayoutInflater().inflate(R.layout.addrepo, null); - final AlertDialog alrt = new AlertDialog.Builder(this).setView(view).create(); + View view = getLayoutInflater(null).inflate(R.layout.addrepo, null); + final AlertDialog alrt = new AlertDialog.Builder(getActivity()).setView(view).create(); final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri); final EditText fingerprintEditText = (EditText) view.findViewById(R.id.edit_fingerprint); - List repos = getRepos(); - final Repo repo = newAddress != null && isImportingRepo ? getRepoByAddress(newAddress, repos) : null; + final Repo repo = ( newAddress != null && isImportingRepo ) + ? RepoProvider.Helper.findByAddress(getActivity().getContentResolver(), newAddress) + : null; alrt.setIcon(android.R.drawable.ic_menu_add); alrt.setTitle(getString(R.string.repo_add_title)); @@ -312,8 +350,8 @@ public class ManageRepo extends ListActivity { new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - setResult(Activity.RESULT_CANCELED); - finish(); + getActivity().setResult(Activity.RESULT_CANCELED); + getActivity().finish(); return; } }); @@ -338,7 +376,7 @@ public class ManageRepo extends ListActivity { // this entry already exists and is not enabled, offer to enable it if (repo.inuse) { alrt.dismiss(); - Toast.makeText(this, R.string.repo_exists_and_enabled, Toast.LENGTH_LONG).show(); + Toast.makeText(getActivity(), R.string.repo_exists_and_enabled, Toast.LENGTH_LONG).show(); return; } else { overwriteMessage.setText(R.string.repo_exists_enable); @@ -373,12 +411,10 @@ public class ManageRepo extends ListActivity { * 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(); - } + ContentValues values = new ContentValues(2); + values.put(RepoProvider.DataColumns.ADDRESS, address); + values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint); + RepoProvider.Helper.insert(getActivity().getContentResolver(), values); finishedAddingRepo(); } @@ -386,13 +422,10 @@ public class ManageRepo extends ListActivity { * Seeing as this repo already exists, we will force it to be enabled again. */ private void createNewRepo(Repo repo) { + ContentValues values = new ContentValues(1); + values.put(RepoProvider.DataColumns.IN_USE, 1); + RepoProvider.Helper.update(getActivity().getContentResolver(), repo, values); repo.inuse = true; - try { - DB db = DB.getDB(); - db.updateRepoByAddress(repo); - } finally { - DB.releaseDB(); - } finishedAddingRepo(); } @@ -404,17 +437,13 @@ public class ManageRepo extends ListActivity { private void finishedAddingRepo() { changed = true; if (isImportingRepo) { - setResult(Activity.RESULT_OK); - finish(); - } else { - refreshList(); + getActivity().setResult(Activity.RESULT_OK); + getActivity().finish(); } } @Override - public boolean onMenuItemSelected(int featureId, MenuItem item) { - - super.onMenuItemSelected(featureId, item); + public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == ADD_REPO) { showAddRepo(); @@ -424,7 +453,7 @@ public class ManageRepo extends ListActivity { return true; } - return false; + return super.onOptionsItemSelected(item); } /** @@ -432,7 +461,7 @@ public class ManageRepo extends ListActivity { * Otherwise return "https://". */ private String getNewRepoUri() { - ClipboardCompat clipboard = ClipboardCompat.create(this); + ClipboardCompat clipboard = ClipboardCompat.create(getActivity()); String text = clipboard.getText(); if (text != null) { try { @@ -447,46 +476,4 @@ public class ManageRepo extends ListActivity { } return text; } - - @Override - public void finish() { - Intent ret = new Intent(); - 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(); - } - } - } diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java index a41c867d1..59a109b7a 100644 --- a/src/org/fdroid/fdroid/RepoXMLHandler.java +++ b/src/org/fdroid/fdroid/RepoXMLHandler.java @@ -20,6 +20,7 @@ package org.fdroid.fdroid; import android.os.Bundle; +import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.updater.RepoUpdater; import org.xml.sax.Attributes; import org.xml.sax.SAXException; @@ -33,7 +34,7 @@ import java.util.Map; public class RepoXMLHandler extends DefaultHandler { // The repo we're processing. - private DB.Repo repo; + private Repo repo; private Map apps; private List appsList; @@ -62,7 +63,7 @@ public class RepoXMLHandler extends DefaultHandler { private int totalAppCount; - public RepoXMLHandler(DB.Repo repo, List appsList, ProgressListener listener) { + public RepoXMLHandler(Repo repo, List appsList, ProgressListener listener) { this.repo = repo; this.apps = new HashMap(); for (DB.App app : appsList) this.apps.put(app.id, app); @@ -157,7 +158,7 @@ public class RepoXMLHandler extends DefaultHandler { } } else if (curel.equals("added")) { try { - curapk.added = str.length() == 0 ? null : DB.dateFormat + curapk.added = str.length() == 0 ? null : DB.DATE_FORMAT .parse(str); } catch (ParseException e) { curapk.added = null; @@ -204,7 +205,7 @@ public class RepoXMLHandler extends DefaultHandler { curapp.detail_trackerURL = str; } else if (curel.equals("added")) { try { - curapp.added = str.length() == 0 ? null : DB.dateFormat + curapp.added = str.length() == 0 ? null : DB.DATE_FORMAT .parse(str); } catch (ParseException e) { curapp.added = null; @@ -212,7 +213,7 @@ public class RepoXMLHandler extends DefaultHandler { } else if (curel.equals("lastupdated")) { try { curapp.lastUpdated = str.length() == 0 ? null - : DB.dateFormat.parse(str); + : DB.DATE_FORMAT.parse(str); } catch (ParseException e) { curapp.lastUpdated = null; } @@ -281,7 +282,7 @@ public class RepoXMLHandler extends DefaultHandler { } else if (localName.equals("package") && curapp != null && curapk == null) { curapk = new DB.Apk(); curapk.id = curapp.id; - curapk.repo = repo.id; + curapk.repo = repo.getId(); hashType = null; } else if (localName.equals("hash") && curapk != null) { diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index f2c13b760..d161cc5ae 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -41,12 +41,16 @@ import android.os.Parcelable; import android.os.ResultReceiver; import android.os.SystemClock; import android.preference.PreferenceManager; +import android.util.Log; + import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.updater.RepoUpdater; public class UpdateService extends IntentService implements ProgressListener { @@ -280,22 +284,23 @@ public class UpdateService extends IntentService implements ProgressListener { // Grab some preliminary information, then we can release the // database while we do all the downloading, etc... int updates = 0; - List repos; + List repos; List apps; try { DB db = DB.getDB(); - repos = db.getRepos(); apps = db.getApps(false); } finally { DB.releaseDB(); } + repos = RepoProvider.Helper.all(getContentResolver()); + // Process each repo... List updatingApps = new ArrayList(); - Set keeprepos = new TreeSet(); + Set keeprepos = new TreeSet(); boolean changes = false; boolean update; - for (DB.Repo repo : repos) { + for (Repo repo : repos) { if (!repo.inuse) continue; // are we updating all repos, or just one? @@ -306,7 +311,7 @@ public class UpdateService extends IntentService implements ProgressListener { if (address.equals(repo.address)) { update = true; } else { - keeprepos.add(repo.id); + keeprepos.add(repo.getId()); update = false; } } @@ -321,7 +326,7 @@ public class UpdateService extends IntentService implements ProgressListener { updatingApps.addAll(updater.getApps()); changes = true; } else { - keeprepos.add(repo.id); + keeprepos.add(repo.getId()); } } catch (RepoUpdater.UpdateException e) { errmsg += (errmsg.length() == 0 ? "" : "\n") + e.getMessage(); @@ -343,7 +348,7 @@ public class UpdateService extends IntentService implements ProgressListener { // Need to flag things we're keeping despite having received // no data about during the update. (i.e. stuff from a repo // that we know is unchanged due to the etag) - for (int keep : keeprepos) { + for (long keep : keeprepos) { for (DB.App app : apps) { boolean keepapp = false; for (DB.Apk apk : app.apks) { @@ -378,8 +383,6 @@ public class UpdateService extends IntentService implements ProgressListener { db.updateApplication(app); } db.endUpdate(); - for (DB.Repo repo : repos) - db.writeLastEtag(repo); } catch (Exception ex) { db.cancelUpdate(); Log.e("FDroid", "Exception during update processing:\n" diff --git a/src/org/fdroid/fdroid/Utils.java b/src/org/fdroid/fdroid/Utils.java index 37d4920fa..c6b397df2 100644 --- a/src/org/fdroid/fdroid/Utils.java +++ b/src/org/fdroid/fdroid/Utils.java @@ -36,6 +36,7 @@ import java.util.Locale; import android.content.Context; import com.nostra13.universalimageloader.utils.StorageUtils; +import org.fdroid.fdroid.data.Repo; public final class Utils { @@ -159,7 +160,7 @@ public final class Utils { return count; } - public static String formatFingerprint(DB.Repo repo) { + public static String formatFingerprint(Repo repo) { return formatFingerprint(repo.pubkey); } diff --git a/src/org/fdroid/fdroid/compat/SwitchCompat.java b/src/org/fdroid/fdroid/compat/SwitchCompat.java index e683fb625..b5cbb7815 100644 --- a/src/org/fdroid/fdroid/compat/SwitchCompat.java +++ b/src/org/fdroid/fdroid/compat/SwitchCompat.java @@ -1,26 +1,27 @@ package org.fdroid.fdroid.compat; import android.annotation.TargetApi; +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 SwitchCompat extends Compatibility { - protected final ManageRepo activity; + protected final Context context; - protected SwitchCompat(ManageRepo activity) { - this.activity = activity; + protected SwitchCompat(Context context) { + this.context = context; } public abstract CompoundButton createSwitch(); - public static SwitchCompat create(ManageRepo activity) { + public static SwitchCompat create(Context context) { if (hasApi(14)) { - return new IceCreamSwitch(activity); + return new IceCreamSwitch(context); } else { - return new OldSwitch(activity); + return new OldSwitch(context); } } @@ -29,24 +30,24 @@ public abstract class SwitchCompat extends Compatibility { @TargetApi(14) class IceCreamSwitch extends SwitchCompat { - protected IceCreamSwitch(ManageRepo activity) { - super(activity); + protected IceCreamSwitch(Context context) { + super(context); } @Override public CompoundButton createSwitch() { - return new Switch(activity); + return new Switch(context); } } class OldSwitch extends SwitchCompat { - protected OldSwitch(ManageRepo activity) { - super(activity); + protected OldSwitch(Context context) { + super(context); } @Override public CompoundButton createSwitch() { - return new ToggleButton(activity); + return new ToggleButton(context); } } diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index 272a29551..26c1b3582 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -16,11 +16,15 @@ public class DBHelper extends SQLiteOpenHelper { public static final String DATABASE_NAME = "fdroid"; + public static final String TABLE_REPO = "fdroid_repo"; + private static final String CREATE_TABLE_REPO = "create table " - + DB.TABLE_REPO + " (id integer primary key, address text not null, " + + TABLE_REPO + " (_id integer primary key, " + + "address text not null, " + "name text, description text, inuse integer not null, " + "priority integer not null, pubkey text, fingerprint text, " - + "maxage integer not null default 0, version integer not null default 0," + + "maxage integer not null default 0, " + + "version integer not null default 0, " + "lastetag text, lastUpdated string);"; private static final String CREATE_TABLE_APK = "create table " + DB.TABLE_APK @@ -49,7 +53,7 @@ public class DBHelper extends SQLiteOpenHelper { + "ignoreThisUpdate int not null," + "primary key(id));"; - private static final int DB_VERSION = 35; + private static final int DB_VERSION = 37; private Context context; @@ -58,6 +62,79 @@ public class DBHelper extends SQLiteOpenHelper { this.context = context; } + private void populateRepoNames(SQLiteDatabase db, int oldVersion) { + if (oldVersion < 37) { + String[] columns = { "address", "_id" }; + Cursor cursor = db.query(TABLE_REPO, columns, + "name IS NULL OR name = ''", null, null, null, null); + cursor.moveToFirst(); + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + String address = cursor.getString(0); + long id = cursor.getInt(1); + ContentValues values = new ContentValues(1); + values.put("name", Repo.addressToName(address)); + String[] args = { Long.toString( id ) }; + db.update(TABLE_REPO, values, "_id = ?", args); + cursor.moveToNext(); + } + } + } + } + + private void renameRepoId(SQLiteDatabase db, int oldVersion) { + if (oldVersion < 36) { + + Log.d("FDroid", "Renaming " + TABLE_REPO + ".id to _id"); + + db.beginTransaction(); + + try { + // http://stackoverflow.com/questions/805363/how-do-i-rename-a-column-in-a-sqlite-database-table#805508 + String tempTableName = TABLE_REPO + "__temp__"; + db.execSQL("ALTER TABLE " + TABLE_REPO + " RENAME TO " + tempTableName + ";" ); + + // I realise this is available in the CREATE_TABLE_REPO above, + // however I have a feeling that it will need to be the same as the + // current structure of the table as of DBVersion 36, or else we may + // get into strife. For example, if there was a field that + // got removed, then it will break the "insert select" + // statement. Therefore, I've put a copy of CREATE_TABLE_REPO + // here that is the same as it was at DBVersion 36. + String createTableDdl = "create table " + TABLE_REPO + " (" + + "_id integer not null primary key, " + + "address text not null, " + + "name text, " + + "description text, " + + "inuse integer not null, " + + "priority integer not null, " + + "pubkey text, " + + "fingerprint text, " + + "maxage integer not null default 0, " + + "version integer not null default 0, " + + "lastetag text, " + + "lastUpdated string);"; + + db.execSQL(createTableDdl); + + String nonIdFields = "address, name, description, inuse, priority, " + + "pubkey, fingerprint, maxage, version, lastetag, lastUpdated"; + + String insertSql = "INSERT INTO " + TABLE_REPO + + "(_id, " + nonIdFields + " ) " + + "SELECT id, " + nonIdFields + " FROM " + tempTableName + ";"; + + db.execSQL(insertSql); + db.execSQL("DROP TABLE " + tempTableName + ";"); + db.setTransactionSuccessful(); + } catch (Exception e) { + Log.e("FDroid", "Error renaming id to _id: " + e.getMessage()); + } + db.endTransaction(); + } + } + @Override public void onCreate(SQLiteDatabase db) { @@ -80,7 +157,7 @@ public class DBHelper extends SQLiteOpenHelper { values.put("inuse", 1); values.put("priority", 10); values.put("lastetag", (String) null); - db.insert(DB.TABLE_REPO, null, values); + db.insert(TABLE_REPO, null, values); values = new ContentValues(); values.put("address", @@ -97,7 +174,7 @@ public class DBHelper extends SQLiteOpenHelper { values.put("inuse", 0); values.put("priority", 20); values.put("lastetag", (String) null); - db.insert(DB.TABLE_REPO, null, values); + db.insert(TABLE_REPO, null, values); } @Override @@ -118,6 +195,8 @@ public class DBHelper extends SQLiteOpenHelper { addMaxAgeToRepo(db, oldVersion); addVersionToRepo(db, oldVersion); addLastUpdatedToRepo(db, oldVersion); + renameRepoId(db, oldVersion); + populateRepoNames(db, oldVersion); } /** @@ -126,13 +205,13 @@ public class DBHelper extends SQLiteOpenHelper { */ private void migradeRepoTable(SQLiteDatabase db, int oldVersion) { if (oldVersion < 20) { - List oldrepos = new ArrayList(); - Cursor c = db.query(DB.TABLE_REPO, + List oldrepos = new ArrayList(); + Cursor c = db.query(TABLE_REPO, new String[] { "address", "inuse", "pubkey" }, null, null, null, null, null); c.moveToFirst(); while (!c.isAfterLast()) { - DB.Repo repo = new DB.Repo(); + Repo repo = new Repo(); repo.address = c.getString(0); repo.inuse = (c.getInt(1) == 1); repo.pubkey = c.getString(2); @@ -140,16 +219,16 @@ public class DBHelper extends SQLiteOpenHelper { c.moveToNext(); } c.close(); - db.execSQL("drop table " + DB.TABLE_REPO); + db.execSQL("drop table " + TABLE_REPO); db.execSQL(CREATE_TABLE_REPO); - for (DB.Repo repo : oldrepos) { + for (Repo repo : oldrepos) { ContentValues values = new ContentValues(); values.put("address", repo.address); values.put("inuse", repo.inuse); values.put("priority", 10); values.put("pubkey", repo.pubkey); values.put("lastetag", (String) null); - db.insert(DB.TABLE_REPO, null, values); + db.insert(TABLE_REPO, null, values); } } } @@ -160,19 +239,19 @@ public class DBHelper extends SQLiteOpenHelper { */ private void addNameAndDescriptionToRepo(SQLiteDatabase db, int oldVersion) { if (oldVersion < 21) { - if (!columnExists(db, DB.TABLE_REPO, "name")) - db.execSQL("alter table " + DB.TABLE_REPO + " add column name text"); - if (!columnExists(db, DB.TABLE_REPO, "description")) - db.execSQL("alter table " + DB.TABLE_REPO + " add column description text"); + if (!columnExists(db, TABLE_REPO, "name")) + db.execSQL("alter table " + TABLE_REPO + " add column name text"); + if (!columnExists(db, TABLE_REPO, "description")) + db.execSQL("alter table " + TABLE_REPO + " add column description text"); ContentValues values = new ContentValues(); values.put("name", context.getString(R.string.default_repo_name)); values.put("description", context.getString(R.string.default_repo_description)); - db.update(DB.TABLE_REPO, values, "address = ?", new String[]{ + db.update(TABLE_REPO, values, "address = ?", new String[]{ context.getString(R.string.default_repo_address)}); values.clear(); values.put("name", context.getString(R.string.default_repo_name2)); values.put("description", context.getString(R.string.default_repo_description2)); - db.update(DB.TABLE_REPO, values, "address = ?", new String[] { + db.update(TABLE_REPO, values, "address = ?", new String[] { context.getString(R.string.default_repo_address2) }); } @@ -184,44 +263,44 @@ public class DBHelper extends SQLiteOpenHelper { */ private void addFingerprintToRepo(SQLiteDatabase db, int oldVersion) { if (oldVersion < 29) { - if (!columnExists(db, DB.TABLE_REPO, "fingerprint")) - db.execSQL("alter table " + DB.TABLE_REPO + " add column fingerprint text"); - List oldrepos = new ArrayList(); - Cursor c = db.query(DB.TABLE_REPO, + if (!columnExists(db, TABLE_REPO, "fingerprint")) + db.execSQL("alter table " + TABLE_REPO + " add column fingerprint text"); + List oldrepos = new ArrayList(); + Cursor c = db.query(TABLE_REPO, new String[] { "address", "pubkey" }, null, null, null, null, null); c.moveToFirst(); while (!c.isAfterLast()) { - DB.Repo repo = new DB.Repo(); + Repo repo = new Repo(); repo.address = c.getString(0); repo.pubkey = c.getString(1); oldrepos.add(repo); c.moveToNext(); } c.close(); - for (DB.Repo repo : oldrepos) { + for (Repo repo : oldrepos) { ContentValues values = new ContentValues(); values.put("fingerprint", DB.calcFingerprint(repo.pubkey)); - db.update(DB.TABLE_REPO, values, "address = ?", new String[] { repo.address }); + db.update(TABLE_REPO, values, "address = ?", new String[] { repo.address }); } } } private void addMaxAgeToRepo(SQLiteDatabase db, int oldVersion) { if (oldVersion < 30) { - db.execSQL("alter table " + DB.TABLE_REPO + " add column maxage integer not null default 0"); + db.execSQL("alter table " + TABLE_REPO + " add column maxage integer not null default 0"); } } private void addVersionToRepo(SQLiteDatabase db, int oldVersion) { - if (oldVersion < 33 && !columnExists(db, DB.TABLE_REPO, "version")) { - db.execSQL("alter table " + DB.TABLE_REPO + " add column version integer not null default 0"); + if (oldVersion < 33 && !columnExists(db, TABLE_REPO, "version")) { + db.execSQL("alter table " + TABLE_REPO + " add column version integer not null default 0"); } } private void addLastUpdatedToRepo(SQLiteDatabase db, int oldVersion) { - if (oldVersion < 35 && !columnExists(db, DB.TABLE_REPO, "lastUpdated")) { - db.execSQL("Alter table " + DB.TABLE_REPO + " add column lastUpdated string"); + if (oldVersion < 35 && !columnExists(db, TABLE_REPO, "lastUpdated")) { + db.execSQL("Alter table " + TABLE_REPO + " add column lastUpdated string"); } } @@ -230,7 +309,7 @@ public class DBHelper extends SQLiteOpenHelper { .putBoolean("triedEmptyUpdate", false).commit(); db.execSQL("drop table " + DB.TABLE_APP); db.execSQL("drop table " + DB.TABLE_APK); - db.execSQL("update " + DB.TABLE_REPO + " set lastetag = NULL"); + db.execSQL("update " + TABLE_REPO + " set lastetag = NULL"); createAppApk(db); } diff --git a/src/org/fdroid/fdroid/data/FDroidProvider.java b/src/org/fdroid/fdroid/data/FDroidProvider.java new file mode 100644 index 000000000..cd8308bd2 --- /dev/null +++ b/src/org/fdroid/fdroid/data/FDroidProvider.java @@ -0,0 +1,59 @@ +package org.fdroid.fdroid.data; + +import android.content.ContentProvider; +import android.content.UriMatcher; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +abstract class FDroidProvider extends ContentProvider { + + public static final String AUTHORITY = "org.fdroid.fdroid.data"; + + protected static final int CODE_LIST = 1; + protected static final int CODE_SINGLE = 2; + + private DBHelper dbHelper; + + abstract protected String getTableName(); + + abstract protected String getProviderName(); + + @Override + public boolean onCreate() { + dbHelper = new DBHelper(getContext()); + return true; + } + + protected final DBHelper db() { + return dbHelper; + } + + protected final SQLiteDatabase read() { + return db().getReadableDatabase(); + } + + protected final SQLiteDatabase write() { + return db().getWritableDatabase(); + } + + @Override + public String getType(Uri uri) { + String type; + switch(getMatcher().match(uri)) { + case CODE_LIST: + type = "dir"; + break; + + case CODE_SINGLE: + default: + type = "item"; + break; + + } + return "vnd.android.cursor." + type + "/vnd." + AUTHORITY + "." + getProviderName(); + } + + abstract protected UriMatcher getMatcher(); + +} + diff --git a/src/org/fdroid/fdroid/data/Repo.java b/src/org/fdroid/fdroid/data/Repo.java new file mode 100644 index 000000000..ca01bde09 --- /dev/null +++ b/src/org/fdroid/fdroid/data/Repo.java @@ -0,0 +1,176 @@ +package org.fdroid.fdroid.data; + +import android.content.ContentValues; +import android.database.Cursor; +import android.util.Log; +import org.fdroid.fdroid.DB; + +import java.net.MalformedURLException; +import java.net.URL; +import java.text.ParseException; +import java.util.Date; + +public class Repo { + + private long id; + + public String address; + public String name; + public String description; + public int version; // index version, i.e. what fdroidserver built it - 0 if not specified + public boolean inuse; + public int priority; + public String pubkey; // null for an unsigned repo + 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; + + public Repo() { + + } + + public Repo(Cursor cursor) { + for(int i = 0; i < cursor.getColumnCount(); i ++ ) { + String column = cursor.getColumnName(i); + if (column.equals(RepoProvider.DataColumns._ID)) { + id = cursor.getInt(i); + } else if (column.equals(RepoProvider.DataColumns.LAST_ETAG)) { + lastetag = cursor.getString(i); + } else if (column.equals(RepoProvider.DataColumns.ADDRESS)) { + address = cursor.getString(i); + } else if (column.equals(RepoProvider.DataColumns.DESCRIPTION)) { + description = cursor.getString(i); + } else if (column.equals(RepoProvider.DataColumns.FINGERPRINT)) { + fingerprint = cursor.getString(i); + } else if (column.equals(RepoProvider.DataColumns.IN_USE)) { + inuse = cursor.getInt(i) == 1; + } else if (column.equals(RepoProvider.DataColumns.LAST_UPDATED)) { + String dateString = cursor.getString(i); + if (dateString != null) { + try { + lastUpdated = DB.DATE_FORMAT.parse(dateString); + } catch (ParseException e) { + Log.e("FDroid", "Error parsing date " + dateString); + } + } + } else if (column.equals(RepoProvider.DataColumns.MAX_AGE)) { + maxage = cursor.getInt(i); + } else if (column.equals(RepoProvider.DataColumns.VERSION)) { + version = cursor.getInt(i); + } else if (column.equals(RepoProvider.DataColumns.NAME)) { + name = cursor.getString(i); + } else if (column.equals(RepoProvider.DataColumns.PUBLIC_KEY)) { + pubkey = cursor.getString(i); + } else if (column.equals(RepoProvider.DataColumns.PRIORITY)) { + priority = cursor.getInt(i); + } + } + } + + public long getId() { return id; } + + public String getName() { + return name; + } + + public String toString() { + return address; + } + + public int getNumberOfApps() { + DB db = DB.getDB(); + int count = db.countAppsForRepo(id); + DB.releaseDB(); + return count; + } + + public boolean isSigned() { + return this.pubkey != null && this.pubkey.length() > 0; + } + + public boolean hasBeenUpdated() { + return this.lastetag != null; + } + /** + * 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 static String addressToName(String address) { + String tempName; + try { + URL url = new URL(address); + tempName = url.getHost() + url.getPath(); + } catch (MalformedURLException e) { + tempName = address; + } + return tempName; + } + + private static int toInt(Integer value) { + if (value == null) { + return 0; + } else { + return value; + } + } + + public void setValues(ContentValues values) { + + if (values.containsKey(RepoProvider.DataColumns._ID)) { + id = toInt(values.getAsInteger(RepoProvider.DataColumns._ID)); + } + + if (values.containsKey(RepoProvider.DataColumns.LAST_ETAG)) { + lastetag = values.getAsString(RepoProvider.DataColumns.LAST_ETAG); + } + + if (values.containsKey(RepoProvider.DataColumns.ADDRESS)) { + address = values.getAsString(RepoProvider.DataColumns.ADDRESS); + } + + if (values.containsKey(RepoProvider.DataColumns.DESCRIPTION)) { + description = values.getAsString(RepoProvider.DataColumns.DESCRIPTION); + } + + if (values.containsKey(RepoProvider.DataColumns.FINGERPRINT)) { + fingerprint = values.getAsString(RepoProvider.DataColumns.FINGERPRINT); + } + + if (values.containsKey(RepoProvider.DataColumns.IN_USE)) { + inuse = toInt(values.getAsInteger(RepoProvider.DataColumns.FINGERPRINT)) == 1; + } + + if (values.containsKey(RepoProvider.DataColumns.LAST_UPDATED)) { + String dateString = values.getAsString(RepoProvider.DataColumns.LAST_UPDATED); + if (dateString != null) { + try { + lastUpdated = DB.DATE_FORMAT.parse(dateString); + } catch (ParseException e) { + Log.e("FDroid", "Error parsing date " + dateString); + } + } + } + + if (values.containsKey(RepoProvider.DataColumns.MAX_AGE)) { + maxage = toInt(values.getAsInteger(RepoProvider.DataColumns.MAX_AGE)); + } + + if (values.containsKey(RepoProvider.DataColumns.VERSION)) { + version = toInt(values.getAsInteger(RepoProvider.DataColumns.VERSION)); + } + + if (values.containsKey(RepoProvider.DataColumns.NAME)) { + name = values.getAsString(RepoProvider.DataColumns.NAME); + } + + if (values.containsKey(RepoProvider.DataColumns.PUBLIC_KEY)) { + pubkey = values.getAsString(RepoProvider.DataColumns.PUBLIC_KEY); + } + + if (values.containsKey(RepoProvider.DataColumns.PRIORITY)) { + priority = toInt(values.getAsInteger(RepoProvider.DataColumns.PRIORITY)); + } + } +} diff --git a/src/org/fdroid/fdroid/data/RepoProvider.java b/src/org/fdroid/fdroid/data/RepoProvider.java new file mode 100644 index 000000000..8232d7cf9 --- /dev/null +++ b/src/org/fdroid/fdroid/data/RepoProvider.java @@ -0,0 +1,294 @@ +package org.fdroid.fdroid.data; + +import android.content.*; +import android.database.Cursor; +import android.net.Uri; +import android.provider.BaseColumns; +import android.text.TextUtils; +import android.util.Log; +import org.fdroid.fdroid.DB; +import org.fdroid.fdroid.FDroidApp; + +import java.util.ArrayList; +import java.util.List; + +public class RepoProvider extends FDroidProvider { + + public static final class Helper { + + private Helper() {} + + public static Repo findById(ContentResolver resolver, long repoId) { + return findById(resolver, repoId, DataColumns.ALL); + } + + public static Repo findById(ContentResolver resolver, long repoId, + String[] projection) { + Uri uri = RepoProvider.getContentUri(repoId); + Cursor cursor = resolver.query(uri, projection, null, null, null); + Repo repo = null; + if (cursor != null) { + cursor.moveToFirst(); + repo = new Repo(cursor); + } + return repo; + } + + public static Repo findByAddress(ContentResolver resolver, + String address) { + return findByAddress(resolver, address, DataColumns.ALL); + } + + public static Repo findByAddress(ContentResolver resolver, + String address, String[] projection) { + List repos = findBy( + resolver, DataColumns.ADDRESS, address, projection); + return repos.size() > 0 ? repos.get(0) : null; + } + + public static List all(ContentResolver resolver) { + return all(resolver, DataColumns.ALL); + } + + public static List all(ContentResolver resolver, String[] projection) { + Uri uri = RepoProvider.getContentUri(); + Cursor cursor = resolver.query(uri, projection, null, null, null); + return cursorToList(cursor); + } + + private static List findBy(ContentResolver resolver, + String fieldName, + String fieldValue, + String[] projection) { + Uri uri = RepoProvider.getContentUri(); + String[] args = { fieldValue }; + Cursor cursor = resolver.query( + uri, projection, fieldName + " = ?", args, null ); + return cursorToList(cursor); + } + + private static List cursorToList(Cursor cursor) { + List repos = new ArrayList(); + if (cursor != null) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + repos.add(new Repo(cursor)); + cursor.moveToNext(); + } + } + return repos; + } + + public static void update(ContentResolver resolver, Repo repo, + ContentValues values) { + + // Change the name to the new address. Next time we update the repo + // index file, it will populate the name field with the proper + // name, but the best we can do is guess right now. + if (values.containsKey(DataColumns.ADDRESS) && + !values.containsKey(DataColumns.NAME)) { + String name = Repo.addressToName(values.getAsString(DataColumns.ADDRESS)); + values.put(DataColumns.NAME, name); + } + + // Recalculate the fingerprint if we are saving a public key (it is + // probably a new public key. If not, we will get the same + // fingerprint anyhow). + if (values.containsKey(DataColumns.PUBLIC_KEY) && + values.containsKey(DataColumns.FINGERPRINT)) { + + String publicKey = values.getAsString(DataColumns.PUBLIC_KEY); + String fingerprint = values.getAsString(DataColumns.FINGERPRINT); + if (publicKey != null && fingerprint == null) { + values.put(DataColumns.FINGERPRINT, DB.calcFingerprint(publicKey)); + } + } + + if (values.containsKey(DataColumns.IN_USE)) { + Integer inUse = values.getAsInteger(DataColumns.IN_USE); + if (inUse != null && inUse == 0) { + values.put(DataColumns.LAST_ETAG, (String)null); + } + } + + Uri uri = getContentUri(repo.getId()); + String[] args = { Long.toString(repo.getId()) }; + resolver.update(uri, values, DataColumns._ID + " = ?", args ); + repo.setValues(values); + } + + /** + * This doesn't do anything other than call "insert" on the content + * resolver, but I thought I'd put it here in the interests of having + * each of the CRUD methods available in the helper class. + */ + public static void insert(ContentResolver resolver, + ContentValues values) { + Uri uri = RepoProvider.getContentUri(); + resolver.insert(uri, values); + } + + public static void remove(ContentResolver resolver, long repoId) { + Uri uri = RepoProvider.getContentUri(repoId); + resolver.delete(uri, null, null); + } + + public static void purgeApps(Repo repo, FDroidApp app) { + // TODO: Once we have content providers for apps and apks, use them + // to do this... + DB db = DB.getDB(); + try { + db.purgeApps(repo, app); + } finally { + DB.releaseDB(); + } + } + + } + + public interface DataColumns extends BaseColumns { + public static String ADDRESS = "address"; + public static String NAME = "name"; + public static String DESCRIPTION = "description"; + public static String IN_USE = "inuse"; + public static String PRIORITY = "priority"; + public static String PUBLIC_KEY = "pubkey"; + public static String FINGERPRINT = "fingerprint"; + public static String MAX_AGE = "maxage"; + public static String LAST_ETAG = "lastetag"; + public static String LAST_UPDATED = "lastUpdated"; + public static String VERSION = "version"; + + public static String[] ALL = { + _ID, ADDRESS, NAME, DESCRIPTION, IN_USE, PRIORITY, PUBLIC_KEY, + FINGERPRINT, MAX_AGE, LAST_UPDATED, LAST_ETAG, VERSION + }; + } + + private static final String PROVIDER_NAME = "RepoProvider"; + + private static final UriMatcher matcher = new UriMatcher(-1); + + static { + matcher.addURI(AUTHORITY, PROVIDER_NAME, CODE_LIST); + matcher.addURI(AUTHORITY, PROVIDER_NAME + "/#", CODE_SINGLE); + } + + public static Uri getContentUri() { + return Uri.parse("content://" + AUTHORITY + "/" + PROVIDER_NAME); + } + + public static Uri getContentUri(long repoId) { + return ContentUris.withAppendedId(getContentUri(), repoId); + } + + @Override + protected String getTableName() { + return DBHelper.TABLE_REPO; + } + + @Override + protected String getProviderName() { + return "RepoProvider"; + } + + protected UriMatcher getMatcher() { + return matcher; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + + switch (matcher.match(uri)) { + case CODE_LIST: + if (TextUtils.isEmpty(sortOrder)) { + sortOrder = "_ID ASC"; + } + break; + + case CODE_SINGLE: + selection = ( selection == null ? "" : selection ) + + "_ID = " + uri.getLastPathSegment(); + break; + + default: + Log.e("FDroid", "Invalid URI for repo content provider: " + uri); + throw new UnsupportedOperationException("Invalid URI for repo content provider: " + uri); + } + + Cursor cursor = read().query(getTableName(), projection, selection, + selectionArgs, null, null, sortOrder); + cursor.setNotificationUri(getContext().getContentResolver(), uri); + return cursor; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + + if (!values.containsKey(DataColumns.ADDRESS)) { + throw new UnsupportedOperationException("Cannot add repo without an address."); + } + + // The following fields have NOT NULL constraints in the DB, so need + // to be present. + + if (!values.containsKey(DataColumns.IN_USE)) { + values.put(DataColumns.IN_USE, 1); + } + + if (!values.containsKey(DataColumns.PRIORITY)) { + values.put(DataColumns.PRIORITY, 10); + } + + if (!values.containsKey(DataColumns.MAX_AGE)) { + values.put(DataColumns.MAX_AGE, 0); + } + + if (!values.containsKey(DataColumns.VERSION)) { + values.put(DataColumns.VERSION, 0); + } + + if (!values.containsKey(DataColumns.NAME)) { + String address = values.getAsString(DataColumns.ADDRESS); + values.put(DataColumns.NAME, Repo.addressToName(address)); + } + + long id = write().insertOrThrow(getTableName(), null, values); + Log.i("FDroid", "Inserted repo. Notifying provider change: '" + uri + "'."); + getContext().getContentResolver().notifyChange(uri, null); + return getContentUri(id); + } + + @Override + public int delete(Uri uri, String where, String[] whereArgs) { + + switch (matcher.match(uri)) { + case CODE_LIST: + // Don't support deleting of multiple repos. + return 0; + + case CODE_SINGLE: + where = ( where == null ? "" : where ) + + "_ID = " + uri.getLastPathSegment(); + break; + + default: + Log.e("FDroid", "Invalid URI for repo content provider: " + uri); + throw new UnsupportedOperationException("Invalid URI for repo content provider: " + uri); + } + + int rowsAffected = write().delete(getTableName(), where, whereArgs); + Log.i("FDroid", "Deleted repos. Notifying provider change: '" + uri + "'."); + getContext().getContentResolver().notifyChange(uri, null); + return rowsAffected; + } + + @Override + public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { + int numRows = write().update(getTableName(), values, where, whereArgs); + Log.i("FDroid", "Updated repo. Notifying provider change: '" + uri + "'."); + getContext().getContentResolver().notifyChange(uri, null); + return numRows; + } + +} diff --git a/src/org/fdroid/fdroid/updater/RepoUpdater.java b/src/org/fdroid/fdroid/updater/RepoUpdater.java index 2893495b8..f77bbccbc 100644 --- a/src/org/fdroid/fdroid/updater/RepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/RepoUpdater.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid.updater; +import android.content.ContentValues; import android.content.Context; import android.os.Bundle; import android.util.Log; @@ -7,6 +8,8 @@ import org.fdroid.fdroid.DB; import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.RepoXMLHandler; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.net.Downloader; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -18,6 +21,7 @@ import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.io.*; import java.util.ArrayList; +import java.util.Date; import java.util.List; abstract public class RepoUpdater { @@ -26,7 +30,7 @@ abstract public class RepoUpdater { public static final int PROGRESS_TYPE_PROCESS_XML = 2; public static final String PROGRESS_DATA_REPO = "repo"; - public static RepoUpdater createUpdaterFor(Context ctx, DB.Repo repo) { + public static RepoUpdater createUpdaterFor(Context ctx, Repo repo) { if (repo.pubkey != null) { return new SignedRepoUpdater(ctx, repo); } else { @@ -35,12 +39,12 @@ abstract public class RepoUpdater { } protected final Context context; - protected final DB.Repo repo; + protected final Repo repo; protected final List apps = new ArrayList(); protected boolean hasChanged = false; protected ProgressListener progressListener; - public RepoUpdater(Context ctx, DB.Repo repo) { + public RepoUpdater(Context ctx, Repo repo) { this.context = ctx; this.repo = repo; } @@ -164,8 +168,6 @@ abstract public class RepoUpdater { if (hasChanged) { downloadedFile = downloader.getFile(); - repo.lastetag = downloader.getETag(); - indexFile = getIndexFromFile(downloadedFile); // Process the index... @@ -184,7 +186,7 @@ abstract public class RepoUpdater { new BufferedReader(new FileReader(indexFile))); reader.parse(is); - updateRepo(handler); + updateRepo(handler, downloader.getETag()); } } catch (SAXException e) { throw new UpdateException( @@ -210,9 +212,15 @@ abstract public class RepoUpdater { } } - private void updateRepo(RepoXMLHandler handler) { + private void updateRepo(RepoXMLHandler handler, String etag) { - boolean repoChanged = false; + ContentValues values = new ContentValues(); + + values.put(RepoProvider.DataColumns.LAST_UPDATED, DB.DATE_FORMAT.format(new Date())); + + if (repo.lastetag == null || !repo.lastetag.equals(etag)) { + values.put(RepoProvider.DataColumns.LAST_ETAG, etag); + } // We read an unsigned index, but that indicates that // a signed version is now available... @@ -224,54 +232,42 @@ abstract public class RepoUpdater { // information as the unsigned one does not... Log.d("FDroid", "Public key found - switching to signed repo for future updates"); - repo.pubkey = handler.getPubKey(); - repoChanged = true; + values.put(RepoProvider.DataColumns.PUBLIC_KEY, handler.getPubKey()); } 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; + values.put(RepoProvider.DataColumns.VERSION, handler.getVersion()); } if (handler.getMaxAge() != -1 && handler.getMaxAge() != repo.maxage) { Log.d("FDroid", "Repo specified a new maximum age - updated"); - repo.maxage = handler.getMaxAge(); - repoChanged = true; + values.put(RepoProvider.DataColumns.MAX_AGE, handler.getMaxAge()); } if (handler.getDescription() != null && !handler.getDescription().equals(repo.description)) { - repo.description = handler.getDescription(); - repoChanged = true; + values.put(RepoProvider.DataColumns.DESCRIPTION, handler.getDescription()); } if (handler.getName() != null && !handler.getName().equals(repo.name)) { - repo.name = handler.getName(); - repoChanged = true; + values.put(RepoProvider.DataColumns.NAME, handler.getName()); } - if (repoChanged) { - try { - DB db = DB.getDB(); - db.updateRepoByAddress(repo); - } finally { - DB.releaseDB(); - } - } + RepoProvider.Helper.update(context.getContentResolver(), repo, values); } public static class UpdateException extends Exception { - public final DB.Repo repo; + public final Repo repo; - public UpdateException(DB.Repo repo, String message) { + public UpdateException(Repo repo, String message) { super(message); this.repo = repo; } - public UpdateException(DB.Repo repo, String message, Exception cause) { + public UpdateException(Repo repo, String message, Exception cause) { super(message, cause); this.repo = repo; } diff --git a/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java b/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java index 0097921d4..8f3f75aaa 100644 --- a/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java @@ -6,6 +6,7 @@ import org.fdroid.fdroid.DB; import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.net.Downloader; import java.io.*; @@ -16,7 +17,7 @@ import java.util.jar.JarFile; public class SignedRepoUpdater extends RepoUpdater { - public SignedRepoUpdater(Context ctx, DB.Repo repo) { + public SignedRepoUpdater(Context ctx, Repo repo) { super(ctx, repo); } diff --git a/src/org/fdroid/fdroid/updater/UnsignedRepoUpdater.java b/src/org/fdroid/fdroid/updater/UnsignedRepoUpdater.java index 6fd6d5699..ec9b0712b 100644 --- a/src/org/fdroid/fdroid/updater/UnsignedRepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/UnsignedRepoUpdater.java @@ -3,13 +3,14 @@ package org.fdroid.fdroid.updater; import android.content.Context; import android.util.Log; import org.fdroid.fdroid.DB; +import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.net.Downloader; import java.io.File; public class UnsignedRepoUpdater extends RepoUpdater { - public UnsignedRepoUpdater(Context ctx, DB.Repo repo) { + public UnsignedRepoUpdater(Context ctx, Repo repo) { super(ctx, repo); } diff --git a/src/org/fdroid/fdroid/views/RepoAdapter.java b/src/org/fdroid/fdroid/views/RepoAdapter.java index 76a5c848e..3fcb3c3aa 100644 --- a/src/org/fdroid/fdroid/views/RepoAdapter.java +++ b/src/org/fdroid/fdroid/views/RepoAdapter.java @@ -1,34 +1,48 @@ package org.fdroid.fdroid.views; +import android.content.Context; +import android.database.Cursor; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.*; -import org.fdroid.fdroid.DB; -import org.fdroid.fdroid.ManageRepo; +import android.widget.CompoundButton; +import android.widget.RelativeLayout; +import android.widget.TextView; import org.fdroid.fdroid.R; import org.fdroid.fdroid.compat.SwitchCompat; +import org.fdroid.fdroid.data.Repo; -import java.util.List; +public class RepoAdapter extends CursorAdapter { -public class RepoAdapter extends BaseAdapter { - - private List repositories; - private final ManageRepo activity; - - public RepoAdapter(ManageRepo activity) { - this.activity = activity; - refresh(); + public interface EnabledListener { + public void onSetEnabled(Repo repo, boolean isEnabled); } - public void refresh() { - try { - DB db = DB.getDB(); - repositories = db.getRepos(); - } finally { - DB.releaseDB(); - } - notifyDataSetChanged(); + private static final int SWITCH_ID = 10000; + + private final LayoutInflater inflater; + + private EnabledListener enabledListener; + + public RepoAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + inflater = LayoutInflater.from(context); + } + + public RepoAdapter(Context context, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + inflater = LayoutInflater.from(context); + } + + public RepoAdapter(Context context, Cursor c) { + super(context, c); + inflater = LayoutInflater.from(context); + } + + public void setEnabledListener(EnabledListener listener) { + enabledListener = listener; } public boolean hasStableIds() { @@ -36,54 +50,45 @@ public class RepoAdapter extends BaseAdapter { } @Override - public int getCount() { - return repositories.size(); + public View newView(Context context, Cursor cursor, ViewGroup parent) { + View view = inflater.inflate(R.layout.repo_item, null); + CompoundButton switchView = addSwitchToView(view, context); + setupView(cursor, view, switchView); + return view; } @Override - public Object getItem(int position) { - return repositories.get(position); + public void bindView(View view, Context context, Cursor cursor) { + CompoundButton switchView = (CompoundButton)view.findViewById(SWITCH_ID); + + // Remove old listener (because we are reusing this view, we don't want + // to invoke the listener for the last repo to use it - particularly + // because we are potentially about to change the checked status + // which would in turn invoke this listener.... + switchView.setOnCheckedChangeListener(null); + setupView(cursor, view, switchView); } - @Override - public long getItemId(int position) { - return getItem(position).hashCode(); - } - private static final int SWITCH_ID = 10000; + private void setupView(Cursor cursor, View view, CompoundButton switchView) { - @Override - public View getView(int position, View view, ViewGroup parent) { + final Repo repo = new Repo(cursor); - 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); - - // Remove old listener (because we are reusing this view, we don't want - // to invoke the listener for the last repo to use it - particularly - // because we are potentially about to change the checked status - // which would in turn invoke this listener.... - switchView.setOnCheckedChangeListener(null); - } - - switchView.setChecked(repository.inuse); + switchView.setChecked(repo.inuse); // Add this listener *after* setting the checked status, so we don't // invoke the listener while setting up the view... switchView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - activity.setRepoEnabled(repository, isChecked); + if (enabledListener != null) { + enabledListener.onSetEnabled(repo, isChecked); + } } }); TextView nameView = (TextView)view.findViewById(R.id.repo_name); - nameView.setText(repository.getName()); + nameView.setText(repo.getName()); RelativeLayout.LayoutParams nameViewLayout = (RelativeLayout.LayoutParams)nameView.getLayoutParams(); nameViewLayout.addRule(RelativeLayout.LEFT_OF, switchView.getId()); @@ -91,17 +96,15 @@ public class RepoAdapter extends BaseAdapter { // 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()) { + if (repo.isSigned()) { signedView.setVisibility(View.INVISIBLE); } else { signedView.setVisibility(View.VISIBLE); } - - return view; } - private CompoundButton addSwitchToView(View parent) { - SwitchCompat switchBuilder = SwitchCompat.create(activity); + private CompoundButton addSwitchToView(View parent, Context context) { + SwitchCompat switchBuilder = SwitchCompat.create(context); CompoundButton switchView = switchBuilder.createSwitch(); switchView.setId(SWITCH_ID); RelativeLayout.LayoutParams layout = new RelativeLayout.LayoutParams( diff --git a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java index edf438d53..b0d4ca81f 100644 --- a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -1,32 +1,23 @@ 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.DB.Repo; import org.fdroid.fdroid.compat.ActionBarCompat; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; 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"; - - private int repoId; +public class RepoDetailsActivity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + long repoId = getIntent().getLongExtra(RepoDetailsFragment.ARG_REPO_ID, 0); + if (savedInstanceState == null) { - RepoDetailsFragment fragment = new RepoDetailsFragment(); - fragment.setRepoChangeListener(this); + RepoDetailsFragment fragment = new RepoDetailsFragment(repoId); fragment.setArguments(getIntent().getExtras()); getSupportFragmentManager() .beginTransaction() @@ -34,48 +25,11 @@ public class RepoDetailsActivity extends FragmentActivity implements RepoDetails .commit(); } - repoId = getIntent().getIntExtra(RepoDetailsFragment.ARG_REPO_ID, -1); + String[] projection = new String[] { RepoProvider.DataColumns.NAME }; + Repo repo = RepoProvider.Helper.findById(getContentResolver(), repoId, projection); - DB db = DB.getDB(); - Repo repo = db.getRepo(repoId); - DB.releaseDB(); - - ActionBarCompat abCompat = ActionBarCompat.create(this); - abCompat.setDisplayHomeAsUpEnabled(true); + ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); setTitle(repo.getName()); } - private void finishWithAction(String actionName) { - Intent data = new Intent(); - data.putExtra(actionName, true); - data.putExtra(DATA_REPO_ID, repoId); - 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... - } - } diff --git a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java index 96d2b3d45..76995486a 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java @@ -2,8 +2,12 @@ package org.fdroid.fdroid.views.fragments; import android.app.Activity; import android.app.AlertDialog; +import android.content.ContentValues; import android.content.DialogInterface; +import android.database.ContentObserver; +import android.net.Uri; import android.os.Bundle; +import android.os.Handler; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; import android.text.Editable; @@ -12,8 +16,8 @@ import android.util.Log; import android.view.*; import android.widget.*; import org.fdroid.fdroid.*; - -import java.util.List; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; public class RepoDetailsFragment extends Fragment { @@ -49,34 +53,15 @@ public class RepoDetailsFragment extends Fragment { 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); + private final long repoId; + public RepoDetailsFragment(long repoId) { + this.repoId = repoId; } // TODO: Currently initialised in onCreateView. Not sure if that is the // best way to go about this... - private DB.Repo repo; + private Repo repo; public void onAttach(Activity activity) { super.onAttach(activity); @@ -88,20 +73,13 @@ public class RepoDetailsFragment extends Fragment { * 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(); - } + private Repo loadRepoDetails() { + return RepoProvider.Helper.findById(getActivity().getContentResolver(), repoId); } 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(); + + repo = loadRepoDetails(); if (repo == null) { Log.e("FDroid", "Error showing details for repo '" + repoId + "'"); @@ -186,7 +164,7 @@ public class RepoDetailsFragment extends Fragment { lastUpdated.setText(lastUpdate); } - private void setupDescription(ViewGroup parent, DB.Repo repo) { + private void setupDescription(ViewGroup parent, Repo repo) { TextView descriptionLabel = (TextView)parent.findViewById(R.id.label_description); TextView description = (TextView)parent.findViewById(R.id.text_description); @@ -210,20 +188,20 @@ public class RepoDetailsFragment extends Fragment { * list can be updated. We will perform the update ourselves though. */ private void performUpdate() { - repo.enable((FDroidApp)getActivity().getApplication()); + // Ensure repo is enabled before updating... + ContentValues values = new ContentValues(1); + values.put(RepoProvider.DataColumns.IN_USE, 1); + RepoProvider.Helper.update(getActivity().getContentResolver(), repo, values); + UpdateService.updateRepoNow(repo.address, 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(); + if (event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES) { + repo = loadRepoDetails(); updateView((ViewGroup)getView()); } } }); - if (repoChangeListener != null) { - repoChangeListener.onUpdatePerformed(repo); - } } /** @@ -238,18 +216,15 @@ public class RepoDetailsFragment extends Fragment { public void afterTextChanged(Editable s) {} @Override + // TODO: This is called each character change, resulting in a DB query. + // Doesn't exactly cause performance problems, + // but seems silly not to go for more of a "focus out" event then + // this "text changed" event. 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); - } + ContentValues values = new ContentValues(1); + values.put(RepoProvider.DataColumns.ADDRESS, s.toString()); + RepoProvider.Helper.update(getActivity().getContentResolver(), repo, values); } } } @@ -293,24 +268,23 @@ public class RepoDetailsFragment extends Fragment { .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... + Repo repo = RepoDetailsFragment.this.repo; + RepoProvider.Helper.remove(getActivity().getContentResolver(), repo.getId()); + getActivity().finish(); + } + }).setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Do nothing... + } } - } ).show(); } - private void setupRepoFingerprint(ViewGroup parent, DB.Repo repo) { + private void setupRepoFingerprint(ViewGroup parent, Repo repo) { TextView repoFingerprintView = (TextView)parent.findViewById(R.id.text_repo_fingerprint); TextView repoFingerprintDescView = (TextView)parent.findViewById(R.id.text_repo_fingerprint_description); From 1f38a84fa9ef9655e336d2255e894309042e2ce6 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 23 Jan 2014 14:02:23 +1100 Subject: [PATCH 039/282] Replaced repoId constructer argument with setArguments() As suggested by android lint. Also removed unused imports. --- .../fdroid/views/RepoDetailsActivity.java | 2 +- .../views/fragments/RepoDetailsFragment.java | 19 +++++++------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java index b0d4ca81f..988138591 100644 --- a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -17,7 +17,7 @@ public class RepoDetailsActivity extends FragmentActivity { long repoId = getIntent().getLongExtra(RepoDetailsFragment.ARG_REPO_ID, 0); if (savedInstanceState == null) { - RepoDetailsFragment fragment = new RepoDetailsFragment(repoId); + RepoDetailsFragment fragment = new RepoDetailsFragment(); fragment.setArguments(getIntent().getExtras()); getSupportFragmentManager() .beginTransaction() diff --git a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java index 76995486a..76aad34f2 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java @@ -4,10 +4,7 @@ import android.app.Activity; import android.app.AlertDialog; import android.content.ContentValues; import android.content.DialogInterface; -import android.database.ContentObserver; -import android.net.Uri; import android.os.Bundle; -import android.os.Handler; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; import android.text.Editable; @@ -53,12 +50,6 @@ public class RepoDetailsFragment extends Fragment { private static final int DELETE = 0; private static final int UPDATE = 1; - private final long repoId; - - public RepoDetailsFragment(long repoId) { - this.repoId = repoId; - } - // TODO: Currently initialised in onCreateView. Not sure if that is the // best way to go about this... private Repo repo; @@ -67,14 +58,18 @@ public class RepoDetailsFragment extends Fragment { super.onAttach(activity); } + private long getRepoId() { + return getArguments().getLong(RepoDetailsFragment.ARG_REPO_ID, 0); + } + /** * 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 + * database. However, or local reference to the 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 Repo loadRepoDetails() { - return RepoProvider.Helper.findById(getActivity().getContentResolver(), repoId); + return RepoProvider.Helper.findById(getActivity().getContentResolver(), getRepoId()); } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -82,7 +77,7 @@ public class RepoDetailsFragment extends Fragment { repo = loadRepoDetails(); if (repo == null) { - Log.e("FDroid", "Error showing details for repo '" + repoId + "'"); + Log.e("FDroid", "Error showing details for repo '" + getRepoId() + "'"); return new LinearLayout(container.getContext()); } From ea9dec34b360423514c8145c2c73d97582e88fe7 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 22 Jan 2014 19:43:41 -0500 Subject: [PATCH 040/282] include upper case URI schemes for matching URIs from QR Codes Android's scheme matcher is case-sensitive, so include ALL CAPS versions to support ALL CAPS URLs in QR Codes. QR Codes have a special ALL CAPS mode that uses a reduced character set, making for more compact QR Codes. This reverts: We should not encourage all caps urls 2651b81792bdeed0db3c3960d0b7283536611012 --- AndroidManifest.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 89646a26b..cd450e7d8 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -113,8 +113,16 @@ + + + From bcb7c048b5b8bc45d6affd03735dfb3cfd2cd08e Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 22 Jan 2014 21:38:53 -0500 Subject: [PATCH 041/282] protect ManageRepo from malformed incoming URIs URIs can come from clicking a web page, NFC transmission, QR Code scan, and more. This code stops badly formed Uri strings from crashing F-Droid. It then shows a Toast error message that it can't understand the incoming URI. --- res/values/strings.xml | 1 + src/org/fdroid/fdroid/ManageRepo.java | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index cb945ff4a..e9bda803d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -81,6 +81,7 @@ This repo is already setup, confirm that you want to re-enable it. The incoming repo is already setup and enabled! You must first delete this repo before you can add one with a different key! + Ignoring malformed repo URI: %s The list of used repositories has changed.\nDo you diff --git a/src/org/fdroid/fdroid/ManageRepo.java b/src/org/fdroid/fdroid/ManageRepo.java index 765999a76..8bc0735c3 100644 --- a/src/org/fdroid/fdroid/ManageRepo.java +++ b/src/org/fdroid/fdroid/ManageRepo.java @@ -226,13 +226,20 @@ class RepoListFragment extends ListFragment /* let's see if someone is trying to send us a new repo */ Intent intent = getActivity().getIntent(); - /* an URL from a click or a QRCode scan */ + /* an URL from a click, NFC, QRCode scan, etc */ Uri uri = intent.getData(); if (uri != null) { - // scheme should only ever be pure ASCII aka Locale.ENGLISH - String scheme = intent.getScheme().toLowerCase(Locale.ENGLISH); + // scheme and host should only ever be pure ASCII aka Locale.ENGLISH + String scheme = intent.getScheme(); + String host = uri.getHost(); + if (scheme == null || host == null) { + String msg = String.format(getString(R.string.malformed_repo_uri), uri); + Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG).show(); + return; + } + scheme = scheme.toLowerCase(Locale.ENGLISH); + host = host.toLowerCase(Locale.ENGLISH); String fingerprint = uri.getUserInfo(); - String host = uri.getHost().toLowerCase(Locale.ENGLISH); if (scheme.equals("fdroidrepos") || scheme.equals("fdroidrepo") || scheme.equals("https") || scheme.equals("http")) { @@ -252,7 +259,6 @@ class RepoListFragment extends ListFragment .replace(intent.getScheme(), scheme) // downcase scheme .replace("fdroidrepo", "http"); // make proper URL showAddRepo(uriString, fingerprint); - Log.i("ManageRepo", uriString + " fingerprint: " + fingerprint); } } } From 93de06adeddf0b316428813f1c05261171a21ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 23 Jan 2014 09:47:33 +0100 Subject: [PATCH 042/282] Fix tabbing in RepoDetailsFragment --- .../fdroid/fdroid/views/fragments/RepoDetailsFragment.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java index 76aad34f2..773430e26 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java @@ -58,9 +58,9 @@ public class RepoDetailsFragment extends Fragment { super.onAttach(activity); } - private long getRepoId() { - return getArguments().getLong(RepoDetailsFragment.ARG_REPO_ID, 0); - } + private long getRepoId() { + return getArguments().getLong(RepoDetailsFragment.ARG_REPO_ID, 0); + } /** * After, for example, a repo update, the details will have changed in the From 6819d678a5dbeff6b170c115d31d16e91b23ee52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 23 Jan 2014 09:55:48 +0100 Subject: [PATCH 043/282] Pull from AndroidPinning, which lowers minsdk from 8 to 5 --- extern/AndroidPinning | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/AndroidPinning b/extern/AndroidPinning index 526654e1b..1f23fa382 160000 --- a/extern/AndroidPinning +++ b/extern/AndroidPinning @@ -1 +1 @@ -Subproject commit 526654e1b9997b32e513d58d9094d4c1102a6cb3 +Subproject commit 1f23fa382377302152d2560ba732c1d5873a971c From 50ee88fbc74f2f7db6460c7ff9b21a4e96347127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 23 Jan 2014 18:59:35 +0100 Subject: [PATCH 044/282] Update AndroidPinning, get rid of -t android-17 --- ant-prepare.sh | 2 +- extern/AndroidPinning | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ant-prepare.sh b/ant-prepare.sh index 7de7d1df7..7817f4ed2 100755 --- a/ant-prepare.sh +++ b/ant-prepare.sh @@ -1,6 +1,6 @@ #!/bin/bash -ex android update lib-project -p extern/Universal-Image-Loader/library -android update lib-project -p extern/AndroidPinning -t android-17 +android update lib-project -p extern/AndroidPinning android update lib-project -p extern/MemorizingTrustManager android update project -p . --name F-Droid diff --git a/extern/AndroidPinning b/extern/AndroidPinning index 1f23fa382..5cfc3f51d 160000 --- a/extern/AndroidPinning +++ b/extern/AndroidPinning @@ -1 +1 @@ -Subproject commit 1f23fa382377302152d2560ba732c1d5873a971c +Subproject commit 5cfc3f51dc9437577c1ded7cb5bca48b95eea131 From 510e8e1ba5b9e2b42ae6da8ea4c12cb6c0017178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 29 Jan 2014 01:02:03 +0100 Subject: [PATCH 045/282] Replace HashSet types with Set --- src/org/fdroid/fdroid/DB.java | 5 +++-- .../fdroid/fdroid/compat/SupportedArchitectures.java | 11 ++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index 73099ee64..56c0e070d 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -30,6 +30,7 @@ import java.util.Collections; import java.util.Date; import java.util.Formatter; import java.util.HashMap; +import java.util.Set; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -325,8 +326,8 @@ public class DB { // check if an APK is compatible with the user's device. private static class CompatibilityChecker extends Compatibility { - private HashSet features; - private HashSet cpuAbis; + private Set features; + private Set cpuAbis; private String cpuAbisDesc; private boolean ignoreTouchscreen; diff --git a/src/org/fdroid/fdroid/compat/SupportedArchitectures.java b/src/org/fdroid/fdroid/compat/SupportedArchitectures.java index eccf9e25f..b28708d5a 100644 --- a/src/org/fdroid/fdroid/compat/SupportedArchitectures.java +++ b/src/org/fdroid/fdroid/compat/SupportedArchitectures.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid.compat; +import java.util.Set; import java.util.HashSet; import android.annotation.TargetApi; @@ -8,21 +9,21 @@ import android.os.Build; public class SupportedArchitectures extends Compatibility { - private static HashSet getOneAbi() { - HashSet abis = new HashSet(1); + private static Set getOneAbi() { + Set abis = new HashSet(1); abis.add(Build.CPU_ABI); return abis; } @TargetApi(8) - private static HashSet getTwoAbis() { - HashSet abis = new HashSet(2); + private static Set getTwoAbis() { + Set abis = new HashSet(2); abis.add(Build.CPU_ABI); abis.add(Build.CPU_ABI2); return abis; } - public static HashSet getAbis() { + public static Set getAbis() { if (hasApi(8)) return getTwoAbis(); return getOneAbi(); } From 126d96e4bad94d8c057bf581685de0cc21b0d1f8 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 24 Jan 2014 11:13:17 -0500 Subject: [PATCH 046/282] prevent soft keyboard from popping up on RepoDetails Its quite annoying to have the URI EditText in focus and the soft keyboard pop up when viewing the RepoDetails since the vast majority of the time, the user will be viewing the info there, not editing the URL. This commit just moves the focus to the frame, and prevents the soft keyboard from showing up by default. The user can still click on the URI EditText to edit it and the soft keyboard will pop up. --- AndroidManifest.xml | 3 ++- res/layout/repodetails.xml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index cd450e7d8..f25b64016 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -129,7 +129,8 @@ + android:parentActivityName=".ManageRepo" + android:windowSoftInputMode="stateHidden" /> From 7c67db22f31a507506950e533bee08abf1398d9a Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 24 Jan 2014 11:08:15 -0500 Subject: [PATCH 047/282] reformat repo URIs to allow wifi, fingerprint, etc. in query string Instead of ramming the fingerprint in the user field of the URI, use the query string. Having the fingerprint in the user field was confusing some browsers because it was trying to log in. The query string is the standard place for such meta data, and has lots of room for expansion including things like wifi network names. This will be useful later to determine if both devices are currently on the same wifi network, and if they are local repos, they should try syncing. --- AndroidManifest.xml | 1 + src/org/fdroid/fdroid/ManageRepo.java | 24 ++++++++------ .../fdroid/views/RepoDetailsActivity.java | 32 +++++++++++++++++-- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index f25b64016..44248cfcf 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -23,6 +23,7 @@ + diff --git a/src/org/fdroid/fdroid/ManageRepo.java b/src/org/fdroid/fdroid/ManageRepo.java index 8bc0735c3..47beedbf4 100644 --- a/src/org/fdroid/fdroid/ManageRepo.java +++ b/src/org/fdroid/fdroid/ManageRepo.java @@ -237,27 +237,31 @@ class RepoListFragment extends ListFragment Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG).show(); return; } + if (scheme.equals("FDROIDREPO") || scheme.equals("FDROIDREPOS")) { + /* + * QRCodes are more efficient in all upper case, so QR URIs are + * encoded in all upper case, then forced to lower case. + * Checking if the special F-Droid scheme being all is upper + * case means it should be downcased. + */ + uri = Uri.parse(uri.toString().toLowerCase(Locale.ENGLISH)); + } + // make scheme and host lowercase so they're readable in dialogs scheme = scheme.toLowerCase(Locale.ENGLISH); host = host.toLowerCase(Locale.ENGLISH); - String fingerprint = uri.getUserInfo(); + String fingerprint = uri.getQueryParameter("fingerprint"); 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 - // be forced to lower case. The scheme and host are downcased - // just to make them more readable in the dialog. + /* sanitize and format for function and readability */ String uriString = uri.toString() - .replace(fingerprint + "@", "") // remove fingerprint + .replaceAll("\\?.*$", "") // remove the whole query .replaceAll("/*$", "") // remove all trailing slashes - .replaceAll("/FDROID/REPO$", "/fdroid/repo") - .replaceAll("/FDROID/ARCHIVE$", "/fdroid/archive") .replace(uri.getHost(), host) // downcase host name .replace(intent.getScheme(), scheme) // downcase scheme - .replace("fdroidrepo", "http"); // make proper URL + .replace("fdroidrepo", "http"); // proper repo address showAddRepo(uriString, fingerprint); } } diff --git a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java index 988138591..2ec73b7b7 100644 --- a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -1,7 +1,12 @@ + package org.fdroid.fdroid.views; +import android.net.Uri; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; import android.os.Bundle; import android.support.v4.app.FragmentActivity; +import android.text.TextUtils; import org.fdroid.fdroid.compat.ActionBarCompat; import org.fdroid.fdroid.data.Repo; @@ -10,6 +15,9 @@ import org.fdroid.fdroid.views.fragments.RepoDetailsFragment; public class RepoDetailsActivity extends FragmentActivity { + private WifiManager wifiManager; + private Repo repo; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -25,11 +33,31 @@ public class RepoDetailsActivity extends FragmentActivity { .commit(); } - String[] projection = new String[] { RepoProvider.DataColumns.NAME }; - Repo repo = RepoProvider.Helper.findById(getContentResolver(), repoId, projection); + String[] projection = new String[] { + RepoProvider.DataColumns.NAME, + RepoProvider.DataColumns.ADDRESS, + RepoProvider.DataColumns.FINGERPRINT + }; + repo = RepoProvider.Helper.findById(getContentResolver(), repoId, projection); ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); setTitle(repo.getName()); + + wifiManager = (WifiManager) getSystemService(WIFI_SERVICE); } + protected Uri getSharingUri() { + Uri uri = Uri.parse(repo.address.replaceFirst("http", "fdroidrepo")); + Uri.Builder b = uri.buildUpon(); + b.appendQueryParameter("fingerprint", repo.fingerprint); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + String ssid = wifiInfo.getSSID().replaceAll("^\"(.*)\"$", "$1"); + String bssid = wifiInfo.getBSSID(); + if (!TextUtils.isEmpty(bssid)) { + b.appendQueryParameter("bssid", Uri.encode(bssid)); + if (!TextUtils.isEmpty(ssid)) + b.appendQueryParameter("ssid", Uri.encode(ssid)); + } + return b.build(); + } } From 7b7da9a1103cba498ea8c207578f9aaa148ca5af Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 24 Jan 2014 11:27:32 -0500 Subject: [PATCH 048/282] when receiving a repo URI with wifi info, warn user if on different wifi A repo URI can include info such as the current wifi SSID and BSSID that the repo is hosted on. This is for when local repos are transmitted via QRCode, NFC, etc. The receiver of this URI then checks to make sure it is on the same wifi access point, and warns the user if not. In the future, it should do more than just warn the user, but instead give concrete actions for the user to take, like associating to that wifi. --- res/values/strings.xml | 1 + src/org/fdroid/fdroid/ManageRepo.java | 29 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/res/values/strings.xml b/res/values/strings.xml index e9bda803d..c5c6d3333 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -196,5 +196,6 @@ need to re-enable this repository to install apps from it. %s or later + Your device is not on the same WiFi as the local repo you just added! Try joining this network: %s diff --git a/src/org/fdroid/fdroid/ManageRepo.java b/src/org/fdroid/fdroid/ManageRepo.java index 47beedbf4..0bf667696 100644 --- a/src/org/fdroid/fdroid/ManageRepo.java +++ b/src/org/fdroid/fdroid/ManageRepo.java @@ -22,6 +22,7 @@ package org.fdroid.fdroid; import android.app.Activity; import android.app.AlertDialog; import android.content.ContentValues; +import android.content.Context; import android.content.DialogInterface; import android.support.v4.app.FragmentActivity; import android.support.v4.app.ListFragment; @@ -29,11 +30,14 @@ import android.support.v4.content.CursorLoader; import android.content.Intent; import android.database.Cursor; import android.net.Uri; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; import android.os.Bundle; import android.support.v4.app.LoaderManager; import android.support.v4.app.NavUtils; import android.support.v4.content.Loader; import android.support.v4.view.MenuItemCompat; +import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; @@ -111,6 +115,8 @@ class RepoListFragment extends ListFragment private final int ADD_REPO = 1; private final int UPDATE_REPOS = 2; + private WifiManager wifiManager; + public boolean hasChanged() { return changed; } @@ -263,6 +269,29 @@ class RepoListFragment extends ListFragment .replace(intent.getScheme(), scheme) // downcase scheme .replace("fdroidrepo", "http"); // proper repo address showAddRepo(uriString, fingerprint); + + // if this is a local repo, check we're on the same wifi + String uriBssid = uri.getQueryParameter("bssid"); + if (!TextUtils.isEmpty(uriBssid)) { + if (uri.getPort() != 8888 + && !host.matches("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+")) { + Log.i("ManageRepo", "URI is not local repo: " + uri); + return; + } + Activity a = getActivity(); + if (wifiManager == null) + wifiManager = (WifiManager) a.getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + String bssid = wifiInfo.getBSSID().toLowerCase(Locale.ENGLISH); + uriBssid = Uri.decode(uriBssid).toLowerCase(Locale.ENGLISH); + if (!bssid.equals(uriBssid)) { + String msg = String.format(getString(R.string.not_on_same_wifi), + uri.getQueryParameter("ssid")); + Toast.makeText(a, msg, Toast.LENGTH_LONG).show(); + } + // TODO we should help the user to the right thing here, + // instead of just showing a message! + } } } } From a02f985efaa29371a823266ba2ffbf71218243ae Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 28 Jan 2014 19:02:23 -0500 Subject: [PATCH 049/282] for signed repo with public key, guarantee fingerprint is also set The stored fingerprint is checked when a repo URI is received by FDroid to prevent bad actors from overriding repo configs with other keys. So if the fingerprint is not stored yet, calculate it and store it. If the fingerprint is stored, then check it against the calculated fingerprint just to make sure it is correct. If the fingerprint is empty, then store the calculated one. This was in place before, but it needed to be updated for the new Repo ContentProvider. --- src/org/fdroid/fdroid/data/RepoProvider.java | 37 +++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/org/fdroid/fdroid/data/RepoProvider.java b/src/org/fdroid/fdroid/data/RepoProvider.java index 8232d7cf9..3f955fd37 100644 --- a/src/org/fdroid/fdroid/data/RepoProvider.java +++ b/src/org/fdroid/fdroid/data/RepoProvider.java @@ -15,6 +15,7 @@ import java.util.List; public class RepoProvider extends FDroidProvider { public static final class Helper { + public static final String TAG = "RepoProvider.Helper"; private Helper() {} @@ -91,16 +92,34 @@ public class RepoProvider extends FDroidProvider { values.put(DataColumns.NAME, name); } - // Recalculate the fingerprint if we are saving a public key (it is - // probably a new public key. If not, we will get the same - // fingerprint anyhow). - if (values.containsKey(DataColumns.PUBLIC_KEY) && - values.containsKey(DataColumns.FINGERPRINT)) { - + /* + * If the repo is signed and has a public key, then guarantee that + * the fingerprint is also set. The stored fingerprint is checked + * when a repo URI is received by FDroid to prevent bad actors from + * overriding repo configs with other keys. So if the fingerprint is + * not stored yet, calculate it and store it. If the fingerprint is + * stored, then check it against the calculated fingerprint just to + * make sure it is correct. If the fingerprint is empty, then store + * the calculated one. + */ + if (values.containsKey(DataColumns.PUBLIC_KEY)) { String publicKey = values.getAsString(DataColumns.PUBLIC_KEY); - String fingerprint = values.getAsString(DataColumns.FINGERPRINT); - if (publicKey != null && fingerprint == null) { - values.put(DataColumns.FINGERPRINT, DB.calcFingerprint(publicKey)); + String calcedFingerprint = DB.calcFingerprint(publicKey); + if (values.containsKey(DataColumns.FINGERPRINT)) { + String fingerprint = values.getAsString(DataColumns.FINGERPRINT); + if (!TextUtils.isEmpty(publicKey)) { + if (TextUtils.isEmpty(fingerprint)) { + values.put(DataColumns.FINGERPRINT, calcedFingerprint); + } else if (!fingerprint.equals(calcedFingerprint)) { + // TODO the UI should represent this error! + Log.e(TAG, "The stored and calculated fingerprints do not match!"); + Log.e(TAG, "stored: " + fingerprint); + Log.e(TAG, "calced: " + calcedFingerprint); + } + } + } else if (!TextUtils.isEmpty(publicKey)) { + // no fingerprint in 'values', so put one there + values.put(DataColumns.FINGERPRINT, calcedFingerprint); } } From 04b5db1f4c2585e1a83bd76d7aca6ea5436b25f5 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 28 Jan 2014 20:18:58 -0500 Subject: [PATCH 050/282] update display of signing key fingerprint Update Utils.formatFingerprint() to create a more readible version of the SHA-256 fingerprint of the signing key of the repo. --- res/values/strings.xml | 2 +- src/org/fdroid/fdroid/Utils.java | 52 +++++-------------- .../views/fragments/RepoDetailsFragment.java | 15 +++--- 3 files changed, 22 insertions(+), 47 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index c5c6d3333..558a506b7 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -168,7 +168,7 @@ Unsigned URL Number of apps - Fingerprint of Repo Signing Key (SHA1) + Fingerprint of Repo Signing Key (SHA-256) Description Last update Update diff --git a/src/org/fdroid/fdroid/Utils.java b/src/org/fdroid/fdroid/Utils.java index c6b397df2..949423b15 100644 --- a/src/org/fdroid/fdroid/Utils.java +++ b/src/org/fdroid/fdroid/Utils.java @@ -18,26 +18,20 @@ package org.fdroid.fdroid; -import android.os.Build; -import android.util.Log; +import android.content.Context; + +import com.nostra13.universalimageloader.utils.StorageUtils; import java.io.BufferedReader; import java.io.Closeable; import java.io.File; import java.io.FileReader; -import java.io.InputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.text.SimpleDateFormat; -import java.security.MessageDigest; -import java.util.Formatter; import java.util.Locale; -import android.content.Context; - -import com.nostra13.universalimageloader.utils.StorageUtils; -import org.fdroid.fdroid.data.Repo; - public final class Utils { public static final int BUFFER_SIZE = 4096; @@ -48,8 +42,6 @@ public final class Utils { public static final SimpleDateFormat LOG_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH); - - public static void copy(InputStream input, OutputStream output) throws IOException { copy(input, output, null, null); @@ -160,36 +152,16 @@ public final class Utils { return count; } - public static String formatFingerprint(Repo repo) { - return formatFingerprint(repo.pubkey); + // return a fingerprint formatted for display + public static String formatFingerprint(String fingerprint) { + if (fingerprint.length() != 62) // SHA-256 is 62 hex chars + return "BAD FINGERPRINT"; + String displayFP = fingerprint.substring(0, 2); + for (int i = 2; i < fingerprint.length(); i = i + 2) + displayFP += " " + fingerprint.substring(i, i + 2); + return displayFP; } - 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"); diff --git a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java index 773430e26..1e23b034e 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java @@ -8,6 +8,7 @@ import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.view.*; @@ -286,15 +287,17 @@ public class RepoDetailsFragment extends Fragment { String repoFingerprint; int repoFingerprintColor; - if (repo.pubkey != null && repo.pubkey.length() > 0) { - repoFingerprint = Utils.formatFingerprint(repo.pubkey); - repoFingerprintColor = getResources().getColor(R.color.signed); - repoFingerprintDescView.setVisibility(View.GONE); - } else { - repoFingerprint = getResources().getString(R.string.unsigned); +// TODO show the current state of the signature check, not just whether there is a key or not + if (TextUtils.isEmpty(repo.fingerprint) && TextUtils.isEmpty(repo.pubkey)) { + repoFingerprint = getResources().getString(R.string.unsigned); repoFingerprintColor = getResources().getColor(R.color.unsigned); repoFingerprintDescView.setVisibility(View.VISIBLE); repoFingerprintDescView.setText(getResources().getString(R.string.unsigned_description)); + } else { + // this is based on repo.fingerprint always existing, which it should + repoFingerprint = Utils.formatFingerprint(repo.fingerprint); + repoFingerprintColor = getResources().getColor(R.color.signed); + repoFingerprintDescView.setVisibility(View.GONE); } repoFingerprintView.setText(repoFingerprint); From 772004756e570452f080c611da33ef54d8ded823 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 28 Jan 2014 17:45:34 -0500 Subject: [PATCH 051/282] handle new signed repo with only fingerprint, no pubkey yet A new repo can be added with only the fingerprint of the signing key, while the regular tests are based on the entire public key (repo.pubkey). This checks for the case when a repo only has the fingerprint and no pubkey yet. In that case, it the pubkey presented by the index.jar file against the stored fingerprint. If they match, then the whole pubkey in the index.jar is stored. --- src/org/fdroid/fdroid/DB.java | 27 ++++++++++++++----- .../fdroid/fdroid/updater/RepoUpdater.java | 6 ++--- .../fdroid/updater/SignedRepoUpdater.java | 6 +++-- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index 56c0e070d..e94374bab 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -21,9 +21,8 @@ package org.fdroid.fdroid; import java.io.File; import java.security.MessageDigest; -import java.net.MalformedURLException; -import java.net.URL; -import java.text.ParseException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; @@ -48,6 +47,7 @@ import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.preference.PreferenceManager; +import android.text.TextUtils; import android.text.TextUtils.SimpleStringSplitter; import android.util.DisplayMetrics; import android.util.Log; @@ -419,14 +419,27 @@ public class DB { } } - public static String calcFingerprint(String pubkey) { - String ret = null; - if (pubkey == null) + public static String calcFingerprint(String keyHexString) { + if (TextUtils.isEmpty(keyHexString)) return null; + else + return calcFingerprint(Hasher.unhex(keyHexString)); + } + + public static String calcFingerprint(Certificate cert) { + try { + return calcFingerprint(cert.getEncoded()); + } catch (CertificateEncodingException e) { + return null; + } + } + + public static String calcFingerprint(byte[] key) { + String ret = null; try { // keytool -list -v gives you the SHA-256 fingerprint MessageDigest digest = MessageDigest.getInstance("SHA-256"); - digest.update(Hasher.unhex(pubkey)); + digest.update(key); byte[] fingerprint = digest.digest(); Formatter formatter = new Formatter(new StringBuilder()); for (int i = 1; i < fingerprint.length; i++) { diff --git a/src/org/fdroid/fdroid/updater/RepoUpdater.java b/src/org/fdroid/fdroid/updater/RepoUpdater.java index f77bbccbc..0ab99ba3d 100644 --- a/src/org/fdroid/fdroid/updater/RepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/RepoUpdater.java @@ -31,10 +31,10 @@ abstract public class RepoUpdater { public static final String PROGRESS_DATA_REPO = "repo"; public static RepoUpdater createUpdaterFor(Context ctx, Repo repo) { - if (repo.pubkey != null) { - return new SignedRepoUpdater(ctx, repo); - } else { + if (repo.fingerprint == null && repo.pubkey == null) { return new UnsignedRepoUpdater(ctx, repo); + } else { + return new SignedRepoUpdater(ctx, repo); } } diff --git a/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java b/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java index 8f3f75aaa..d8925c304 100644 --- a/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java @@ -7,7 +7,6 @@ import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.net.Downloader; import java.io.*; import java.security.cert.Certificate; @@ -31,7 +30,10 @@ public class SignedRepoUpdater extends RepoUpdater { boolean match = false; for (Certificate cert : certs) { String certdata = Hasher.hex(cert); - if (repo.pubkey.equals(certdata)) { + if (repo.pubkey == null && repo.fingerprint.equals(DB.calcFingerprint(cert))) { + repo.pubkey = certdata; + } + if (repo.pubkey != null && repo.pubkey.equals(certdata)) { match = true; break; } From 4a7f0ef9a213e43ab15843cf2473077a69bab37c Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 30 Jan 2014 10:12:06 +1100 Subject: [PATCH 052/282] Resolve memory issue when updating repo. Solves issue # 453. Previously, it was reading one line at a time of the index file. Turns out that the entire index is only on about 5 lines, thus the 5th line is about 2mb. The solution here is to switch to reading a fixed length of 4096 characters at a time rather than entire lines. This ends up a bit more complex (because I wrote a custom tokenizer rather than being able to use Java String methods such as indexOf). There are still other memory issues on low memory devices, to do with trying to parse ~1000 app value objects into memory, each with numerous string objects associated with them. But that particular issue will be solved in the future, with the ContentProvider stuff. --- src/org/fdroid/fdroid/Utils.java | 53 ++++++++++++-------------------- 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/src/org/fdroid/fdroid/Utils.java b/src/org/fdroid/fdroid/Utils.java index 949423b15..767b30048 100644 --- a/src/org/fdroid/fdroid/Utils.java +++ b/src/org/fdroid/fdroid/Utils.java @@ -22,13 +22,7 @@ import android.content.Context; import com.nostra13.universalimageloader.utils.StorageUtils; -import java.io.BufferedReader; -import java.io.Closeable; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; import java.text.SimpleDateFormat; import java.util.Locale; @@ -117,37 +111,30 @@ public final class Utils { public static int countSubstringOccurrence(File file, String substring) throws IOException { int count = 0; - BufferedReader reader = null; + FileReader input = null; try { + int currentSubstringIndex = 0; + char[] buffer = new char[4096]; - reader = new BufferedReader(new FileReader(file)); - while(true) { - String line = reader.readLine(); - if (line == null) { - break; + input = new FileReader(file); + int numRead = input.read(buffer); + while(numRead != -1) { + + for (char c : buffer) { + if (c == substring.charAt(currentSubstringIndex)) { + currentSubstringIndex ++; + if (currentSubstringIndex == substring.length()) { + count ++; + currentSubstringIndex = 0; + } + } else { + currentSubstringIndex = 0; + } } - count += countSubstringOccurrence(line, substring); + numRead = input.read(buffer); } - } finally { - closeQuietly(reader); - } - return count; - } - - /** - * Thanks to http://stackoverflow.com/a/767910 - */ - public static int countSubstringOccurrence(String toSearch, String substring) { - int count = 0; - int index = 0; - while (true) { - index = toSearch.indexOf(substring, index); - if (index == -1){ - break; - } - count ++; - index += substring.length(); + closeQuietly(input); } return count; } From f098b9c6d82c60a1561e0aea2f27f55e7c7f77fd Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 30 Jan 2014 20:22:35 +1100 Subject: [PATCH 053/282] Trim search string before querying for apps. Fixes issue #452. It turns out that recent versions of android do this automatically, but my gingerbread emulator didn't. In the process, I also refactored the getQuery method. It was previously a void method that modified the state of the search view, which is a bit counter-intuitive given it's name. Instead, I made it just a getter, which calculates the query and returns it. That way, I was able to remove mQuery from the fields in the search view. Generally, the less state we need to worry about (e.g. fields in an object), the less assumptions we need to make about whether that field has been set or not. --- src/org/fdroid/fdroid/SearchResults.java | 49 ++++++++++++++---------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/org/fdroid/fdroid/SearchResults.java b/src/org/fdroid/fdroid/SearchResults.java index 72d71efd2..a9ea1d02b 100644 --- a/src/org/fdroid/fdroid/SearchResults.java +++ b/src/org/fdroid/fdroid/SearchResults.java @@ -48,23 +48,22 @@ public class SearchResults extends ListActivity { private AppListAdapter applist; - private String mQuery; - - protected void getQuery(Intent intent) { + protected String getQuery() { + Intent intent = getIntent(); + String query; if (Intent.ACTION_SEARCH.equals(intent.getAction())) { - mQuery = intent.getStringExtra(SearchManager.QUERY); + query = intent.getStringExtra(SearchManager.QUERY); } else { Uri data = intent.getData(); - if (data.isHierarchical()) { - mQuery = data.getQueryParameter("q"); - if (mQuery.startsWith("pname:")) - mQuery = mQuery.substring(6); + if (data != null && data.isHierarchical()) { + query = data.getQueryParameter("q"); + if (query != null && query.startsWith("pname:")) + query = query.substring(6); } else { - mQuery = data.getEncodedSchemeSpecificPart(); + query = data.getEncodedSchemeSpecificPart(); } } - if (mQuery == null || mQuery.length() == 0) - finish(); + return query; } @Override @@ -79,25 +78,34 @@ public class SearchResults extends ListActivity { // Start a search by just typing setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); + } - getQuery(getIntent()); - + @Override + protected void onResume() { + super.onResume(); updateView(); } @Override protected void onNewIntent(Intent intent) { - getQuery(intent); super.onNewIntent(intent); - updateView(); + setIntent(intent); } private void updateView() { + String query = getQuery(); + + if (query != null) + query = query.trim(); + + if (query == null || query.length() == 0) + finish(); + List matchingids = new ArrayList(); try { DB db = DB.getDB(); - matchingids = db.doSearch(mQuery); + matchingids = db.doSearch(query.trim()); } catch (Exception ex) { Log.d("FDroid", "Search failed - " + ex.getMessage()); } finally { @@ -107,7 +115,6 @@ public class SearchResults extends ListActivity { List apps = new ArrayList(); List allApps = ((FDroidApp) getApplication()).getApps(); for (DB.App app : allApps) { - boolean include = false; for (String id : matchingids) { if (id.equals(app.id)) { apps.add(app); @@ -119,14 +126,14 @@ public class SearchResults extends ListActivity { TextView tv = (TextView) findViewById(R.id.description); String headertext; if (apps.size() == 0) { - headertext = getString(R.string.searchres_noapps, mQuery); + headertext = getString(R.string.searchres_noapps, query); } else if (apps.size() == 1) { - headertext = getString(R.string.searchres_oneapp, mQuery); + headertext = getString(R.string.searchres_oneapp, query); } else { - headertext = getString(R.string.searchres_napps, apps.size(), mQuery); + headertext = getString(R.string.searchres_napps, apps.size(), query); } tv.setText(headertext); - Log.d("FDroid", "Search for '" + mQuery + "' returned " + apps.size() + Log.d("FDroid", "Search for '" + query + "' returned " + apps.size() + " results"); applist.clear(); for (DB.App app : apps) { From d553b07af2b095b35d3dc72818a9daab9e3cb287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 29 Jan 2014 23:33:22 +0100 Subject: [PATCH 054/282] Update submodules --- extern/Universal-Image-Loader | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/Universal-Image-Loader b/extern/Universal-Image-Loader index 1c2a91e46..45e5f6768 160000 --- a/extern/Universal-Image-Loader +++ b/extern/Universal-Image-Loader @@ -1 +1 @@ -Subproject commit 1c2a91e464b49874068a8bf2a6e39d39aae9208a +Subproject commit 45e5f67683430d55b5a6ea65130307d6b45ded67 From b8998d711aeadb49227dfee591786b286dbe1dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 29 Jan 2014 23:33:39 +0100 Subject: [PATCH 055/282] Run fix-ellipsis.sh --- res/values-bg/strings.xml | 2 +- res/values-ca/strings.xml | 4 ++-- res/values-el/strings.xml | 4 ++-- res/values-es/strings.xml | 4 ++-- res/values-eu/strings.xml | 4 ++-- res/values-fa/strings.xml | 4 ++-- res/values-fi/strings.xml | 4 ++-- res/values-fr/strings.xml | 2 +- res/values-gl/strings.xml | 2 +- res/values-it/strings.xml | 4 ++-- res/values-ko/strings.xml | 2 +- res/values-nb/strings.xml | 4 ++-- res/values-nl/strings.xml | 4 ++-- res/values-pl/strings.xml | 4 ++-- res/values-pt-rBR/strings.xml | 4 ++-- res/values-ro/strings.xml | 4 ++-- res/values-ru/strings.xml | 4 ++-- res/values-sl/strings.xml | 2 +- res/values-sr/strings.xml | 4 ++-- res/values-sv/strings.xml | 2 +- res/values-tr/strings.xml | 2 +- res/values-uk/strings.xml | 2 +- 22 files changed, 36 insertions(+), 36 deletions(-) diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml index 5071c0d5a..08b4285e4 100644 --- a/res/values-bg/strings.xml +++ b/res/values-bg/strings.xml @@ -54,7 +54,7 @@ %d налични актуализации. Актуализации на F-Droid са налични Моля изчакай - Обновявани на списъка с приложения... + Обновявани на списъка с приложения… Взимане на приложението от Адрес на хранилището Списъкът на хранилищата е променен. diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index cf4c87e5c..2ce09afeb 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -57,7 +57,7 @@ L\'adreça d\'un dipòsit té un aspecte com ara: http://f-droid.org/repoHi ha %d actualitzacions disponibles. Hi ha actualitzacions de l\'F-Droid disponibles Un moment si us plau - S\'està actualitzant la llista d\'aplicacions... + S\'està actualitzant la llista d\'aplicacions… S\'està obtenint l\'aplicació des de Adreça del dipòsit Aquest repositori ja existeix. @@ -114,7 +114,7 @@ La voleu actualitzar? %1$s S\'està connectant a %1$s - S\'està comprovant la compatibilitat de les aplicacions amb el vostre dispositiu... + S\'està comprovant la compatibilitat de les aplicacions amb el vostre dispositiu… No es fa servir cap permís. Permisos de la versió %s Mostra els permisos diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index 219366daa..20951b2f5 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -56,7 +56,7 @@ %d διαθέσιμες ενημερώσεις. Διαθέσιμες ενημερώσεις για το F-Droid Παρακαλώ περιμένετε - Ενημέρωση λίστα εφαρμογών... + Ενημέρωση λίστα εφαρμογών… Λήψη εφαρμογών από Διεύθυνση αποθετηρίου Η λίστα με τα χρησιμοποιούμενα αποθετήρια έχει αλλάξει. @@ -112,7 +112,7 @@ %1$s Σύνδεση με %1$s - Έλεγχος συμβατότητας εφαρμογών με τη συσκευή σας... + Έλεγχος συμβατότητας εφαρμογών με τη συσκευή σας… Δεν χρησιμοποιείται καμία άδεια. Άδειες για την έκδοση %s Εμφάνιση αδειών diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 5cbbcc4fe..32c11dcb0 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -56,7 +56,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo %d actualizaciones disponibles. Actualizaciones de F-Droid disponibles Por favor, espera - Actualizando la lista de aplicaciones... + Actualizando la lista de aplicaciones… Obteniendo la aplicación de Dirección del repositorio La lista de repositorios usada ha cambiado. @@ -112,7 +112,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo %1$s Conectando a %1$s - Comprobando la compatibilidad de las aplicaciones con tu dispositivo... + Comprobando la compatibilidad de las aplicaciones con tu dispositivo… No se usan permisos. Permisos para la versión %s Mostrar permisos diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index 17251f6f0..38580082f 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -42,7 +42,7 @@ GNU GPLv3 lizentziapean argitaratua. %d eguneraketa eskuragarri. F-Droid eguneraketak eskuragarri Mesedez itxaron - Aplikazio-zerrenda eguneratzen... + Aplikazio-zerrenda eguneratzen… Aplikazioa eskuratzen hemendik Biltegiaren helbidea Erabilitako biltegien zerrenda aldatu egin da. @@ -82,7 +82,7 @@ Eguneratu nahi dituzu? Azkenaldian eguneratua %1$s(e)ra konektatzen - Aplikazioak zure gailuarekin bateragarriak diren egiaztatzen... + Aplikazioak zure gailuarekin bateragarriak diren egiaztatzen… Ez da baimenik erabiltzen. %s bertsioarentzako baimenak Erakutsi baimenak diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml index d5eba10c2..c038cad7f 100644 --- a/res/values-fa/strings.xml +++ b/res/values-fa/strings.xml @@ -56,7 +56,7 @@ %d به‌روزرسانی موجود است. به‌روزرسانی‌های F-Droid موجود هستند لطفاً صبر کنید - به‌روزرسانی فهرست برنامه‌ها... + به‌روزرسانی فهرست برنامه‌ها… گرفتن برنامه از نشانی مخزن فهرست مخزن‌ها تغییر یافته‌است. @@ -112,7 +112,7 @@ %1$s اتصال به %1$s - بررسی سازگاری برنامه‌ها با دستگاه شما... + بررسی سازگاری برنامه‌ها با دستگاه شما… دسترسی‌ای استفاده نشده‌است. دسترسی‌های نسخهٔ %s نمایش دسترسی‌ها diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml index 30c1de051..1f15cb520 100644 --- a/res/values-fi/strings.xml +++ b/res/values-fi/strings.xml @@ -40,7 +40,7 @@ %d päivitystä saatavilla. F-Droid: Päivityksiä saatavilla Odota hetki - Päivitetään sovelluslistaa... + Päivitetään sovelluslistaa… Haetaan sovellusta lähteestä Sovelluslähteen osoite Lista käytetyistä sovelluslähteistä on muuttumut. @@ -81,7 +81,7 @@ Tahdotko päivittää ne? Kaikki Uutta Viimeaikoina päivitetty - Tarkistetaan ohjelman yhteensopivuutta laitteesi kanssa... + Tarkistetaan ohjelman yhteensopivuutta laitteesi kanssa… Teema Valitse käytettävä teema diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index aad768058..66a541eb6 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -56,7 +56,7 @@ L\'URL d\'un dépôt ressemble à ceci : http://f-droid.org/repo %d mises à jour sont disponibles. Des mises à jour F-Droid sont disponibles Patientez - Mise à jour de la liste d\'applications... + Mise à jour de la liste d\'applications… Réception d\'application de Adresse du dépôt La liste des dépôts utilisés a changé. diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml index a68ac09e5..3ad07e478 100644 --- a/res/values-gl/strings.xml +++ b/res/values-gl/strings.xml @@ -60,7 +60,7 @@ Un enderezo a un repositorio sería algo %d actualizacións dispoñíbeis Actualizacións de F-Droid dispoñíbeis Agarde por favor - Actualizando a lista de aplicativos... + Actualizando a lista de aplicativos… Obtención do aplicativo desde Enderezo do repositorio Cambiou a lista de repositorios usados. diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 38d0b678b..8c0b047a9 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -56,7 +56,7 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo %d aggiornamenti disponibili. Aggiornamenti per F-Droid Disponibili Attendere prego - Aggiornamento elenco applicazioni... + Aggiornamento elenco applicazioni… Scaricamento applicazione da Indirizzo repository L\'elenco dei repository in uso è cambiato. @@ -112,7 +112,7 @@ Vuoi aggiornarlo? %1$s Connessione a %1$s - Controllo compatibilità applicazioni con il tuo dispositivo... + Controllo compatibilità applicazioni con il tuo dispositivo… Non viene usata alcuna autorizzazione. Autorizzazioni per la versione %s Mostra autorizzazioni diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index b4657fd03..4ebb9f90f 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -47,7 +47,7 @@ %d개의 업데이트를 사용할 수 있습니다. F-Droid 업데이트를 사용할 수 있습니다. 잠시만 기다려주세요 - 응용 프로그램 목록 업데이트중... + 응용 프로그램 목록 업데이트중… 에서 응용프로그램 가져오기 저장소 주소 사용된 저장소의 목록이 변경되었습니다. diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index 224f4be4d..7f6f6ed65 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -52,7 +52,7 @@ Lisensiert GNU GPLv3. %d oppdateringer tilgjengelig. F-Droid: Oppdateringer tilgjengelig Vennligst vent - Oppdaterer applikasjonsliste... + Oppdaterer applikasjonsliste… Henter program fra Registeradresse Listen over brukte register har endret seg. Vil du oppdatere dem? @@ -107,7 +107,7 @@ Lisensiert GNU GPLv3. %1$s Kobler til %1$s - Sjekker programstøtte for ditt utstyr... + Sjekker programstøtte for ditt utstyr… Krever ingen tillatelser. Tillatelser for versjon %s Vis tillatelser diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index 3bb122e70..3c7c5bf94 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -59,7 +59,7 @@ Een bron-adres ziet er ongeveer %d vernieuwingen zijn beschikbaar F-Droid Vernieuwingen Beschikbaar Even geduld aub - Applicatie-lijst vernieuwen... + Applicatie-lijst vernieuwen… downloaden applicatie van Bron-adres De lijst van gebruikte bronnen is veranderd. @@ -115,7 +115,7 @@ Wilt u ze vernieuwen? %1$s Connecteren naar %1$s - Controleer app compatibiliteit met uw apparaat... + Controleer app compatibiliteit met uw apparaat… Geen permissies worden gebruikt Permissies voor versie %s Laat permissies zien diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 3fc646fea..a4cd61bac 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -50,7 +50,7 @@ Wydany na licencji GNU GPLv3. Dostępnych jest %d uaktualnień Uaktualnienie F-Droid jest dostępne Proszę czekać - Aktualizowanie listy aplikacji... + Aktualizowanie listy aplikacji… Pobieranie aplikacji z Adres repozytorium odcisk (opcjonalnie) @@ -99,7 +99,7 @@ Czy chcesz je zaktualizować? Przetwarzanie aplikacji %2$d / %3$d z %1$s Trwa łączenie z %1$s - Sprawdzanie kompatybilności aplikacji z urządzeniem... + Sprawdzanie kompatybilności aplikacji z urządzeniem… Uprawnienia dla wersji %s Wyświetl uprawnienia Wyświetlaj listę uprawnień wymaganych przez aplikację diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index 858e5f162..22909f99e 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -56,7 +56,7 @@ Um endereço do repositório é algo similar a isto: http://f-droid.org/repo%d atualizações disponíveis. Atualizações do F-Droid Disponíveis Aguarde - Atualizando a lista de aplicativos... + Atualizando a lista de aplicativos… Baixando aplicativo de Endereço do repositório A lista de repositórios usados mudou. @@ -112,7 +112,7 @@ Você deseja atualizá-los? %1$s Conectando-se a %1$s - Verificando compatibilidade de aplicativos com o seu dispositivo... + Verificando compatibilidade de aplicativos com o seu dispositivo… Nenhuma permissão utilizada. Permissões para a versão %s Mostrar permissões diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml index d5cd2dfbf..6c2a501b3 100644 --- a/res/values-ro/strings.xml +++ b/res/values-ro/strings.xml @@ -32,6 +32,6 @@ Distribuit sub licenta GNU GPLv3. Instalat Disponibil Actualizare - Asteptati ... - Se actualizeaza lista ... + Asteptati … + Se actualizeaza lista … diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 3064836d7..b603fcb90 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -51,7 +51,7 @@ Доступно 1 обновление. Обновлений доступно - %d. Подождите - Список приложений обновляется... + Список приложений обновляется… Взять приложение из Адрес репозитория Список репозиториев изменился. @@ -94,7 +94,7 @@ %1$s Соединение с %1$s - Проверка совместимости приложений с устройством... + Проверка совместимости приложений с устройством… Разрешений не требуется. Разрешения для версии %s Показывать разрешения diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index 361dba0aa..041c75ec1 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -35,7 +35,7 @@ Izdan z licenco GNU GPLv3. Na razpolago Posodobitve Počakajte prosim - Poteka posodobitev spiska aplikacij ... + Poteka posodobitev spiska aplikacij … Prejem aplikacije iz Naslov skladišča Spisek uporabljenih skladišč se je spremenil. diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index 596a8efe5..fb2d511b5 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -56,7 +56,7 @@ %d нове/нових верзија на располагању Ажурирање Ф-Дроида на располагању. Сачекајте - Ажурира се листа апликација... + Ажурира се листа апликација… Скида се апликација са Адреса ризнице Промењена је листа ризница у употреби. @@ -109,7 +109,7 @@ %1$s Повезивање са %1$s - Проверава се да ли је апликација компатибилна са вашим уређајем... + Проверава се да ли је апликација компатибилна са вашим уређајем… Не захтевају се никакве дозволе. Дозволе за верзију %s Прикажи дозволе diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index 9128e8f9b..c3f9e8390 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -59,7 +59,7 @@ En förrådsadress ser ut så här: https://f-droid.org/repo %d uppdateringar finns tillgängliga. Uppdateringar för F-Droid tillgängliga Var vänlig vänta - Uppdaterar programlistan... + Uppdaterar programlistan… Hämtar program från Förrådsadress fingeravtryck (valfritt) diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml index 34a6b4cc1..c4698caf7 100644 --- a/res/values-tr/strings.xml +++ b/res/values-tr/strings.xml @@ -56,7 +56,7 @@ Bir depo adresi şuna benzer: https://f-droid.org/repo %d güncelleme bulunmaktadır. F-Droid güncellemeleri bulunmaktadır Bekleyiniz - Uygulama listesi güncelleniyor... + Uygulama listesi güncelleniyor… Uygulama buradan alınıyor: Depo adresi Kullanılan depoların listesi değişti. diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index a9336a1fc..6bfa64745 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -35,7 +35,7 @@ Наявне Оновлення Зачекайте - Оновлюю список програм... + Оновлюю список програм… Звантажую програму Адреса репозиторію Список репозиторіїв змінено. From cb68daa7f9d413897dc5476ddc441e1a9a8a4a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 29 Jan 2014 23:43:56 +0100 Subject: [PATCH 056/282] New script: remove-unused-trans --- tools/remove-unused-trans.sh | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100755 tools/remove-unused-trans.sh diff --git a/tools/remove-unused-trans.sh b/tools/remove-unused-trans.sh new file mode 100755 index 000000000..7c6e49007 --- /dev/null +++ b/tools/remove-unused-trans.sh @@ -0,0 +1,9 @@ +#!/bin/bash -x + +# Remove extra translations + +lint . --quiet --check ExtraTranslation --nolines | \ + sed -n 's/.*Error: "\([^"]*\)" is translated here but not found in default locale.*/\1/p' | \ + while read name; do + sed -i "/name=\"$name\"/d" res/values-*/strings.xml + done From 102ff2d0be024e38b61b5b65c39b246ca9919ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 29 Jan 2014 23:44:04 +0100 Subject: [PATCH 057/282] Run remove-unused-trans --- res/values-ar/strings.xml | 1 - res/values-bg/strings.xml | 13 ------------- res/values-ca/strings.xml | 17 ----------------- res/values-de/strings.xml | 17 ----------------- res/values-el/strings.xml | 17 ----------------- res/values-eo/strings.xml | 3 --- res/values-es/strings.xml | 17 ----------------- res/values-eu/strings.xml | 12 ------------ res/values-fa/strings.xml | 17 ----------------- res/values-fi/strings.xml | 13 ------------- res/values-fr/strings.xml | 17 ----------------- res/values-gl/strings.xml | 17 ----------------- res/values-it/strings.xml | 17 ----------------- res/values-ko/strings.xml | 14 -------------- res/values-nb/strings.xml | 17 ----------------- res/values-nl/strings.xml | 17 ----------------- res/values-pl/strings.xml | 14 -------------- res/values-pt-rBR/strings.xml | 17 ----------------- res/values-ro/strings.xml | 7 ------- res/values-ru/strings.xml | 14 -------------- res/values-sl/strings.xml | 11 ----------- res/values-sr/strings.xml | 15 --------------- res/values-sv/strings.xml | 17 ----------------- res/values-tr/strings.xml | 17 ----------------- res/values-ug/strings.xml | 17 ----------------- res/values-uk/strings.xml | 11 ----------- res/values-zh-rCN/strings.xml | 11 ----------- 27 files changed, 377 deletions(-) diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml index 6dafe8e8c..69c22fa0e 100644 --- a/res/values-ar/strings.xml +++ b/res/values-ar/strings.xml @@ -3,5 +3,4 @@ عثر على تطبيق واحد يوافق \'%s\': لم يعثر على أي تطبيق يوافق \'%s\' الإصدار - %d إصدار متوفر diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml index 08b4285e4..bb8ac1dd4 100644 --- a/res/values-bg/strings.xml +++ b/res/values-bg/strings.xml @@ -7,20 +7,13 @@ Изглежда този пакет не е съвместим с твоето устойство. Искаш ли да опиташ да го инсталираш въпреки това? Опитваш се да инсталираш по-стара версия на праложението. Това може да го повреди и дори да изтрие данните ти. Искаш ли да го инсталираш въпреки това? Версия - %d налични версии - %d налична версия Кеширай свалените приложения - Пази свалените apk файлове на SD картата Актуализации Други Последно сканиране на хранилищата: %s никога - Автоматично сканиране на хранилищата - Актуализирай списъка на приложенията от хранилищата автоматично Уведомления - Уведомявай ме при нови налични актуализации Актуализирай историята - Дни за показване на нови/актуализирани приложения Резултати от търсенето Детайли за приложението Такова приложение не беше намерено @@ -47,7 +40,6 @@ Отказ Избери хранилище за премахване Актуализирай хранилищата - Инсталирани Налични За актуализация 1 налична актуализация. @@ -80,14 +72,11 @@ Свалянето е отказано Дисплей Експерт - Активирай експертен режим Търсене на приложения Вид на синхронизация на базата данни Съвместимост на приложенията Root достъп - Показвай приложения изискващи root права Игнорирай сензорния екран - Винаги включвай приложения изискващи сензорен екран Всички Какво ново Обновени наскоро @@ -103,8 +92,6 @@ Не се искат разрешения. Разрешения за версия %s Покажи разрешения - Показване на списък с разрешения, които приложението ползва Нямаш инсталирано приложение, което може да изпълни %s Компактно оформление - Показвай само имената и описанията на приложенията в списъка diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index 2ce09afeb..dd14d6c06 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -7,22 +7,14 @@ Sembla que aquest paquet no és compatible amb el vostre dispositiu. Voleu provar a instaŀlar-lo de totes maneres? Aneu a desactualitzar aquesta aplicació. Això podria fer que l\'aplicació no funcionés o inclús es perdessin les vostres dades. Esteu segur que ho voleu fer? Versió - Hi ha %d versions disponibles - Hi ha %d versió disponible Memòria cau de les aplicacions baixades - Desa els fitxers apk baixats a la targeta SD Actualitzacions Altres Darrera actualització dels dipòsits: %s mai - Actualitza automàticament els dipòsits - Actualitza de forma automàtica la llista d\'aplicacions dels dipòsits Només en wifi - Actualitza automàticament les llistes d\'aplicacions només en wifi Notifica-ho - Avisa\'m quan hi hagi noves actualitzacions Actualitzacions - Mostra les aplicacions noves o actualitzades periòdicament Resultats de la cerca Detalls de l\'aplicació No s\'ha trobat l\'aplicació @@ -50,7 +42,6 @@ L\'adreça d\'un dipòsit té un aspecte com ara: http://f-droid.org/repoSobreescriu Trieu el dipòsit que voleu suprimir Actualitza els dipòsits - Instal·lat Disponible Actualitzacions Hi ha 1 actualització disponible. @@ -92,17 +83,12 @@ La voleu actualitzar? Aquesta aplicació depèn d\'altres aplicacions no lliures Pantalla Usuari expert - Activa el mode expert Cerca aplicacions Mode de sincronització de la base de dades - Estableix el valor de l\'etiqueta de sincronització de SQLite Compatibilitat de les aplicacions Versions incompatibles - Mostra versions d\'aplicacions que siguin incompatibles amb el dispositiu Root - Mostra aplicacions que necessiten privilegis de root Ignora la pantalla tàctil - Inclou sempre les aplicacions que necesiten de la pantalla tàctil Tot Novetats S\'ha actualitzat fa poc @@ -118,10 +104,7 @@ La voleu actualitzar? No es fa servir cap permís. Permisos de la versió %s Mostra els permisos - Mostra els permisos que necessita l\'aplicació No teniu cap aplicació disponible que pugui gestionar %s Vista compacta - Mostra només els noms de les aplicacions i els resums a la llista Tema - Escull un tema a utilitzar diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 213e2b4b8..e184664e1 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -7,22 +7,14 @@ Es sieht so aus, als sei dieses Paket mit Ihrem Gerät nicht kompatibel. Möchten Sie trotzdem versuchen es zu installieren? Sie versuchen eine vorherige Version einer bereits installierten Anwendung zu installieren. Dies kann zu Fehlverhalten der Anwendung und gegebenenfalls zu Datenverlust führen. Möchten Sie dennoch fortfahren? Version - %d Versionen verfügbar - %d Version verfügbar Heruntergeladene Anwendungen zwischenspeichern - Heruntergeladene Anwendungspakete auf der SD-Karte belassen Aktualisierungen Andere Letzte Aktualisierung der Paketquellen: %s niemals - Automatische Paketaktualisierung - Anwendungsliste automatisch aus den Paketquellen aktualisieren Nur über WLAN - Anwendungsliste nur über WLAN automatisch aktualisieren Benachrichtigen - Benachrichtigen, wenn neue Aktualisierungen verfügbar sind Aktualisierungsverlauf - Zeitraum in Tagen, für den neue bzw. aktualisierte Anwendungen angezeigt werden. Suchergebnisse Anwendungsdetails Keine passende Anwendung gefunden @@ -53,7 +45,6 @@ Die Adresse einer Paketquelle sieht etwa so aus: https://f-droid.org/repoÜberschreiben Zu entfernende Paketquelle auswählen Paketquellen aktualisieren - Installiert Verfügbar Aktualisierungen Eine Aktualisierung ist verfügbar. @@ -101,17 +92,12 @@ Sollen diese aktualisiert werden? Der Originalcode ist nicht völlig frei Anzeige Experte - Expertenmodus einschalten Anwendungen suchen Datenbanksynchronisierungsart - SQLite-Synchronisationsmodus einstellen Kompatibilität der Anwendung Inkompatible Versionen - Anwendungsversionen anzeigen, welche mit dem Gerät inkompatibel sind Root - Anwendungen anzeigen, die Root-Rechte benötigen Touchscreen ignorieren - Anwendungen die einen Touchscreen benötigen immer mit anzeigen Alle Was gibt es Neues Kürzlich Aktualisiert @@ -127,10 +113,7 @@ Sollen diese aktualisiert werden? Es werden keine Berechtigungen verwendet. Berechtigungen für Version %s Berechtigungen anzeigen - Eine Liste von Berechtigungen die von einer Anwendung benötigt werden anzeigen Es ist keine Anwendung installiert, die mit %s umgehen kann Kompakte Ansicht - Nur Namen und Kurzbeschreibung in der Anwendungsliste anzeigen Thema - Ein Thema welches benutzt werden soll auswählen diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index 20951b2f5..b7eef575a 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -7,22 +7,14 @@ Φαίνεται ότι αυτό το πακέτο δεν είναι συμβατό με τη συσκευή σας. Θα θέλατε να δοκιμάσετε να το εγκαταστήσετε ούτως ή άλλως; Προσπαθείτε να υποβαθμίσετε αυτήν την εφαρμογή. Αν το κάνετε αυτό, μπορεί να προκληθούν προβλήματα στην εφαρμογή ή ακόμα και να χάσετε τα δεδομένα σας. Θέλετε να δοκιμάσετε να την υποβαθμίσετε ούτως ή άλλως; Έκδοση - %d διαθέσιμες εκδόσεις - %d διαθέσιμη έκδοση Αποθήκευση ληφθέντων εφαρμογών στην προσωρινή μνήμη - Διατήρηση ληφθέντων αρχείων apk στην κάρτα SD Ενημερώσεις Άλλα Τελευταίο σάρωμα αποθετηρίου: %s ποτέ - Αυτόματη Σάρωση Αποθετηρίου - Αυτόματη ενημέρωση της λίστας εφαρμογών από το αποθετήριο Μόνο σε wifi - Αυτόματη ενημέρωση της λίστας εφαρμογών μόνο σε wifi Ειδοποίηση - Ειδοποίηση για την ύπαρξη διαθέσιμων ενημερώσεων Ιστορικό ενημερώσεων - Ημέρες για να εμφανίζονται οι νέες/ενημερωμένες εφαρμογές Αποτελέσματα Αναζήτησης Λεπτομέρειες Εφαρμογής Δεν βρέθηκε τέτοια εφαρμογή @@ -49,7 +41,6 @@ Ακύρωση Επιλογή αποθετηρίου για διαγραφή Ενημέρωση αποθετηρίων - Εγκατεστημένο Διαθέσιμα Ενημερώσεις 1 διαθέσιμη ενημερώση. @@ -90,17 +81,12 @@ Αυτή η εφαρμογή εξαρτάται από άλλες μη-ελεύθερες εφαρμογές. Εμφάνιση Για Προχωρημένους - Ενεργοποίηση λειτουργίας για προχωρημένους Αναζήτηση εφαρμογών Λειτουργία συγχρονισμόυ της βάσης δεδομένων - Ορισμός τιμής για SQLite synchronous flag Συμβατότητα εφαμοργής Μη συμβατές εκδόσεις - Εμφάνιση εκδόσεων εφαρμογών που δεν είναι συμβατές με τη συσκευή Root - Εμφάνιση εφαρμογών που απαιτούν δικαιώματα root Αγνόησε την Οθόνη Επαφής - Να συμπεριλαμβάνονται πάντα εφαρμογές που απαιτούν οθόνη επαφής Όλα Τι νέο υπάρχει Πρόσφατα Ενημερωμένες @@ -116,10 +102,7 @@ Δεν χρησιμοποιείται καμία άδεια. Άδειες για την έκδοση %s Εμφάνιση αδειών - Εμφάνιση λίστας αδειών που χρειάζεται μια εφαρμογή Δεν έχεται καμία διαθέσιμη εφαρμογή που να μπορεί να χειριστεί %s Συμπτυγμένη Διάταξη - Εμφάνιση μόνο ονομάτων εφαρμογών και περίληψεων στη λίστα Θέμα - Επιλέξτε θέμα προς χρήση diff --git a/res/values-eo/strings.xml b/res/values-eo/strings.xml index 29b7f290b..1d61d54e6 100644 --- a/res/values-eo/strings.xml +++ b/res/values-eo/strings.xml @@ -1,8 +1,6 @@ Versio - %d versioj disponeblaj - %d versio disponebla Ĝisdatigoj Sciigi Pri F-Droid @@ -19,7 +17,6 @@ Aldoni Rezigni Ĝisdatigi deponejojn - Instalitaj Disponeblaj Ĝisdatigoj Bonvolu atendi diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 32c11dcb0..996cdf9b9 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -7,22 +7,14 @@ Parece que este paquete no es compatible con tu dispositivo. ¿Quieres probar e instalarlo de todos modos? Estás intentando instalar una versión inferior de esta aplicación. Hacerlo puede derivar en mal funcionamiento o incluso pérdida de datos. ¿Quieres intentarlo de todos modos? Versión - %d versiones disponibles - %d versión disponible Caché de aplicaciones descargadas - Mantener los ficheros apk descargados en la SD card Actualizaciones Otros Último escaneo del repositorio: %s nunca - Escanear los repositorios automáticamente - Actualizar la lista de aplicaciones desde los repositorios automáticamente Sólo con wifi - Actualizar la lista de aplicaciones desde los repositorios automáticamente sólo con wifi Notificar - Notificar cuando haya actualizaciones disponibles Historial de actualizaciones - Días a mostrar apps nuevas/actualizadas Resultados de la búsqueda Detalles de la aplicación No se encontró la aplicación @@ -49,7 +41,6 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Cancelar Elige el repositorio a eliminar Actualizar repositorios - Instalado Disponible Actualizaciones 1 actualización disponible. @@ -90,17 +81,12 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Esta aplicación depende de otras no libres Mostrar Experto - Activa el modo experto Buscar aplicaciones Modo síncrono de base de datos - Fija el valor del flag síncrono de SQLite Compatibilidad de aplicaciones Versiones incompatibles - Muestra versiones de aplicaciones que no sean compatibles con el dispositivo Root - Muestras las aplicaciones que requieren privilegios de root Ignorar pantalla táctil - Siempre incluir aplicaciones que requieren pantalla táctil Todos Novedades Recientemente actualizados @@ -116,10 +102,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo No se usan permisos. Permisos para la versión %s Mostrar permisos - Mostrar una lista de los permisos que necesita una aplicación No tienes instalada ninguna aplicación que pueda manejar %s Diseño compacto - Mostrar sólo los nombres de las aplicaciones y resúmenes en la lista Tema - Escoger un tema a usar diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index 38580082f..b4680d751 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -5,17 +5,11 @@ \'%s\'-rekin bat datorren aplikaziorik ez da aurkitu Bertsio berria zaharraren desberdina den gako batekin sinatuta dago. Bertsio berria instalatzeko, aurretik zaharra desinstalatu beharra dago. Mesedez, egizu eta saiatu berriro. (Kontutan izan desinstalatzean aplikazioak gordetako barne datuak ezabatuko direla) Bertsioa - %d bertsio eskuragarri - Bertsio %d eskuragarri Gorde cache-an deskargatutako aplikazioak - Gorde deskargatutako apk fitxategiak SD txartelean Eguneraketak Biltegiaren azken eskaneatzea: %s inoiz ez - Eskaneatu biltegiak automatikoki - Eguneratu aplikazio-zerrenda biltegiarekin automatikoki Jakinarazi - Jakinarazi eguneraketa berriak eskuragarri daudenean Eguneratu historia F-Droid-i buruz Jatorrian Aptoide-n oinarritua. @@ -35,7 +29,6 @@ GNU GPLv3 lizentziapean argitaratua. Utzi Aukeratu biltegia ezabatzeko Eguneratu biltegiak - Instalatuta Eskuragarri Eguneraketak Eguneraketa 1 eskuragarri. @@ -68,15 +61,11 @@ Eguneratu nahi dituzu? Tämä ohjelma sisältää mainontaa Bistaratu Aditua - Gaitu aditu modua Bilatu aplikazioak Datu-base modu sinkronoa - Ezarri SQLite-ren bandera sinkronoaren balioa Aplikazioen bateragarritasuna Root - Erakutsi root baimenak behar dituzten aplikazioak Ezikusi egin ukipen-pantailari - Sartu ukipen-pantaila behar duten aplikazioak beti Guztia Zer da berria Azkenaldian eguneratua @@ -86,6 +75,5 @@ konektatzen Ez da baimenik erabiltzen. %s bertsioarentzako baimenak Erakutsi baimenak - Bistaratu aplikazio batek behar dituen baimenen zerrenda Diseinu trinkoa diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml index c038cad7f..1d4c28915 100644 --- a/res/values-fa/strings.xml +++ b/res/values-fa/strings.xml @@ -7,22 +7,14 @@ به‌نظر می‌رسد این بسته با دستگاه شما هماهنگ نیست. آیا می‌خواهید آن را به هر قیمتی آزمایش و نصب کنید؟ شما در حال قدیمی‌کردن و کاهش درجهٔ این برنامه هستید. انجام چنین کاری ممکن منجر به خرابی یا از دست رفتن داده‌های شما شود. آیا می‌خواهید سعی کنید این برنامه را به هر قیمتی قدیمی کنید؟ نسخه - %d نسخه موجود است - %d نسخه موجود است میانگیری برنامه‌های دریافت‌شده - نگه‌داشتن پرونده‌های APK دریافت‌شده در کارت SD به‌روزرسانی‌ها دیگر آخرین اسکن مخزن: %S هیچ‌گاه - بررسی خودکار مخزن برنامه‌ها - به‌روزرسانی فهرست برنامه‌ها از مخازن به‌صورت خودکار فقط هنگام اتصال وای‌فای - به‌روزرسانی فهرست برنامه به صورت خودکار فقط هنگام اتصال وای‌فای مطلع‌سازی - هنگامی که به‌روزرسانی‌های جدیدی موجود بود من را مطلع کن به‌روزرسانی تاریخچه - روزهای نمایش برنامه‌های جدید/به‌روزشده جستجوی نتایج مشخصات برنامه چنین برنامه‌ای یافت نشد @@ -49,7 +41,6 @@ فسخ انتخاب مخزن برای حذف به‌روزرسانی مخازن - نصب‌شده موجود به‌روزرسانی‌ها ۱ به‌روزرسانی موجود است. @@ -90,17 +81,12 @@ این برنامه به سایر برنامه‌های غیر آزاد وابسته است نمایش خارج‌سازی - فعال‌سازی حالت خارج‌سازی جستجوی برنامه‌ها حالت هماهنگی پایگاه داده‌ها - تنظیم مقدار پرچم «همزمانی» اس‌کیولایت هماهنگی برنامه نسخه‌های غیرهماهنگ - نسخه‌هایی از برنامه که ناهماهنگ با این دستگاه است را نمایش بده روت - برنامه‌هایی که نیازمند دسترسی روت هستند را نمایش بده صفحهٔ نمایش لمسی را در نظر نگیر - همیشه برنامه‌هایی که نیازمند صفحهٔ نمایش لمسی هستند را شامل کن همه چیزهای جدید اخیراً به‌روز شده @@ -116,10 +102,7 @@ دسترسی‌ای استفاده نشده‌است. دسترسی‌های نسخهٔ %s نمایش دسترسی‌ها - فهرستی از دسترسی‌هایی که یک برنامه نیاز دارد را نشان بده شما برنامه‌ای که بتواند %s را مدیریت کند ندارید طرح‌بندی فشرده - فقط نام‌ها و خلاصه‌ها را در فهرست نمایش بده پوسته - پوسته‌ای برای استفاده انتخاب کنید diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml index 1f15cb520..367475645 100644 --- a/res/values-fi/strings.xml +++ b/res/values-fi/strings.xml @@ -5,17 +5,11 @@ \'%s\':ään täsmääviä sovelluksia ei löytynyt Uusi versio on allekirjoitettu eri avaimella kuin vanha. Asentaaksesi uuden version, vanha täytyy poistaa ensin. Tee tämä ja yritä uudelleen. (Huomaa, että poistaminen poistaa kaiken sovelluksen sisäisen datan) Versio - %d versiota saatavilla - %d versio saatavilla Säilytä ladatut sovellukset välimuistissa - Pidä ladatut apk-tiedostot SD-kortilla Päivitykset Viimeisin sovelluslähteiden skannaus: %s ei koskaan - Automaattinen sovelluslähteen skannaus - Päivitä sovelluslista sovelluslähteistä automaattisesti Huomauta - Ilmoita kun uusia päivityksiä on saatavilla Päivityshistoria Hakutulokset Tietoa F-Droidista @@ -33,7 +27,6 @@ Peruuta Valitse sovelluslähde, jonka tahdot poistaa Päivitä sovelluslähteet - Asennettu Saatavilla Päivityksiä 1 päivitys saatavilla. @@ -67,21 +60,15 @@ Tahdotko päivittää ne? Lataus peruutettu Näyttö Asiantuntija - Ota käyttöön asiantuntija-tila Etsi sovelluksia Tietokannan synkronointi-tila - Aseta SQLiten synkrooninen lippu Sovellusten yhteensopivuus Yhteensopimattomat versiot - Näytä ohjelmaversiot jotka eivät ole yhteensopivia laitteesi kanssa Root - Näytä sovellukset, jotka vaativat root-oikeudet Älä välitä kosketusnäytöstä - Sisällytä aina sovellukset, jotka vaativat kosketusnäytön Kaikki Uutta Viimeaikoina päivitetty Tarkistetaan ohjelman yhteensopivuutta laitteesi kanssa… Teema - Valitse käytettävä teema diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 66a541eb6..55e0d974a 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -7,22 +7,14 @@ Il semble que ce paquet ne soit pas compatible avec votre appareil. Voulez-vous quand même tenter de l\'installer ? Vous essayez de revenir à une ancienne version de cette application. Cela peut causer des problèmes de fonctionnement ou des pertes de données. Voulez-vous tout de même revenir à une ancienne version? Version - %d versions disponibles - %d version disponible Stocker les applications téléchargées sur l\'appareil - Garder les fichiers apk téléchargés sur la carte SD Mises à jour Autres Dernière analyse du dépôt : %s jamais - Balayage automatique du dépôt - Mettre à jour automatiquement la liste d\'applications à partir des dépôts Seulement par WiFi - Mettre à jour automatiquement la liste d\'applications seulement via WiFi Notifier - Avertir quand de nouvelles mises à jour sont disponibles Historique des mises à jour - Jours pour présenter les applications nouvelles/mises à jour Résultats de la recherche Détails de l\'application Pas d\'application trouvée @@ -49,7 +41,6 @@ L\'URL d\'un dépôt ressemble à ceci : http://f-droid.org/repo Annuler Choisissez le dépôt à supprimer Mettre à jour les dépôts - Installée Disponible Mises à jour 1 mise à jour est disponible. @@ -90,17 +81,12 @@ Voulez-vous les mettre à jour ? Cette application dépend d\'autres applications non libres Affichage Expert - Activer le mode expert Rechercher des applications Mode de synchronisation à la base de données - Régler la valeur de la synchronisation SQLite Compatibilité de l\'application Versions incompatibles - Afficher les version des applis qui ne sont pas compatibles avec votre appareil Root - Montrer les applications qui requièrent les privilèges root Ignorer l\'écran tactile - Toujours inclure les applications qui nécessitent un écran tactile Tout Quoi de neuf ? Mis à jour récemment @@ -116,10 +102,7 @@ Voulez-vous les mettre à jour ? Aucune autorisation n\'est utilisée. Autorisations pour la version %s Afficher les autorisations - Afficher la liste des autorisations que nécessite l\'application Vous n\'avez aucune application installée pour gérer %s Affichage compact - Afficher seulement les noms d\'applications et les résumés dans la liste Thème - Sélectionner un thème à utiliser diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml index 3ad07e478..3b28894da 100644 --- a/res/values-gl/strings.xml +++ b/res/values-gl/strings.xml @@ -7,22 +7,14 @@ Parece que este paquete non é compatíbel co seu dispositivo. Quere facer a proba e instalalo aínda así? Está tentando instalar unha versión anterior. Isto pode supoñer un mal funcionamento ou perda de datos. Quere tentalo de todas maneiras? Versión - %d versións dispoñíbeis - %d versión dispoñíbeis Memorizar as aps descargadas - Gardar os ficheiros apk descargados na tarxeta SD Actualizacións Outro Último escaneado do repositorio: %s nunca - Escaneado automático dos repositorios - Actualizar automaticamente a lista de aps do repositorio Soamente no wifi - Actualizar as listas de aps automaticamente soamente cando hai wifi Notificar - Avisarme cando estean dispoñíbeis novas actualizacións Histórico de actualizacións - Días para amosar aps novas/actualizadas Resultados da busca Detalles da ap Non se atopa tal ap @@ -53,7 +45,6 @@ Un enderezo a un repositorio sería algo Cancelar Escoller o repositorio que retirar Actualizar repositorios - Instalado Dispoñíbel Actualizacións 1 Actualización dispoñíbel @@ -94,17 +85,12 @@ Quere actualizalos? Esta ap depende doutras aps non libres Amosar Experto - Activar o modo experto Buscar aplicativos Modo de sincronización da base de datos - Estabelece o valor da marca de sincronización de SQLite Compatibilidade de aplicativos Versións incompatíbeis - Amosar versións de aps que son incompatíbeis co dispositivo Root - Amosar aplicativos que requiren privilexios de root Ignorar a pantalla táctil - Incluír sempre as aps que requiren pantalla táctil Todos Qué novidades hai Actualizado recentemente @@ -120,10 +106,7 @@ Quere actualizalos? Non se usan permisos Permisos para a versión %s Amosar permisos - Presentar a lista dos permisos que precisa unha ap Non ten ningunha ap dispoñíbel que poida manexar %s Deseño compacto - Amosar unicamente os nomes das aps e resumos na lista Tema - Escolla un tema diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 8c0b047a9..fde3ceaff 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -7,22 +7,14 @@ Sembra che questo pacchetto non sia compatibile con il tuo dispositivo. Vuoi provare comunque ad installarlo? Stai provando a passare ad una versione precedente di questa applicazione. Potresti avere malfunzionamenti e perdita di dati. Vuoi installarla comunque? Versione - %d versioni disponibili - %d versione disponibile Cache applicazioni scaricate - Salva su SD i file apk scaricati Aggiornamenti Altro Ultima scansione repository: %s mai - Scansione repository automatica - Aggiorna automaticamente l\'elenco applicazioni Solo su wifi - Aggiorna liste app automaticamente solo su wifi Avviso - Avvisa quando sono disponibili nuovi aggiornamenti Aggiorna i repository - Giorni per mostrare app nuove/da aggiornare Risultati Ricerca Dettagli App Nessuna app corrispondente trovata @@ -49,7 +41,6 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo Annulla Rimuovi repository Aggiorna i repository - Installato Disponibile Aggiornamenti 1 aggiornamento disponibile. @@ -90,17 +81,12 @@ Vuoi aggiornarlo? Questa app dipende da applicazioni non libere Mostra Esperto - Abilita la modalità avanzata Ricerca applicazioni Modalità di sincronizzazione database - Impostazione del flag di sincronizzazione di SQLite Compatibilità applicazioni Versioni incompatibili - Mostra versioni delle app incompatibili con questo dispositivo Amministratore - Mostra le applicazioni che richiedono i privilegi di amministrazione Ignora il Touchscreen - Includi sempre le applicazioni che richiedono il touchscreen Tutte Novità Aggiornate di Recente @@ -116,10 +102,7 @@ Vuoi aggiornarlo? Non viene usata alcuna autorizzazione. Autorizzazioni per la versione %s Mostra autorizzazioni - Mostra la lista di autorizzazioni necessarie per un\'app Non hai alcuna app disponibile che può gestire %s Layout Compatto - Mostra solo nomi e sintesi delle app nella lista Tema - Scegli un tema da usare diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index 4ebb9f90f..28be1501c 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -5,19 +5,12 @@ \'%s\' 와 일치하는 응용프로그램을 찾을 수 없습니다. 이 응용 프로그램의 다운그레이드 하려고 합니다. 이전 버전을 설치할 경우, 응용 프로그램에 데이터가 손상되거나 오작동이 발생할 수 있습니다. 정말로 다운그레이드하시겠습니까? 버전 - %d개의 버전을 사용할 수 있습니다. - %d개의 버전을 사용할 수 있습니다. 다운로드된 설치파일 저장 - SD카드에 다운로드된 설치파일을 보관 업데이트 기타 마지막 저장소 검색: %s - 자동으로 저장소 검색 - 자동으로 저장소의 앱 목록을 갱신합니다. Wi-Fi 연결 시에만 - Wi-Fi에 연결되어 있을 때만 자동으로 앱 목록을 갱신합니다. 알림 - 새로운 업데이트가 가능할 때 알림 이력 업데이트 검색 결과 앱 상세정보 @@ -41,7 +34,6 @@ 취소 제거할 저장소 선택 저장소 업데이트 - 설치됨 업데이트 1개의 업데이트를 사용할 수 있습니다. %d개의 업데이트를 사용할 수 있습니다. @@ -77,14 +69,11 @@ 이 응용프로그램은 활동을 추적하여 리포트를 보고합니다. 표시 전문가 - 전문가 모드 사용 응용 프로그램 검색 데이터베이스 동기화 모드 응용 프로그램 호환성 호환되지 않는 버전 - Root 권한이 필요한 앱을 보여줍니다. 터치스크린 무시 - 터치스크린을 요구하는 앱을 포함합니다 전체 새로운 기능 최근 업데이트 @@ -98,10 +87,7 @@ 사용된 권한이 없습니다. %s 버전에 대한 권한 권한 표시 - 응용 프로그램이 필요한 권한의 목록을 표시 %s을(를) 처리할 수 있는 응용프로그램이 없습니다. 컴팩트 레이아웃 - 목록에 웹이름과 요약정보만 표시합니다. 테마 - 사용할 테마를 선택하세요. diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index 7f6f6ed65..bd501e332 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -7,22 +7,14 @@ Det ser ut til at denne pakken ikke er kompatibel med ditt utstyr. Vil du prøve å installere det likevel? Du prøver å nedgradere denne applikasjonen. Dette kan føre til at applikasjonen henger, og du kan til og med miste dine data. Vil du prøve å nedgradere likevel? Versjon - %d versjoner tilgjengelig - %d versjon tilgjengelig Lagre nedlastede applikasjoner i buffer - Behold nedlastede apk-filer på minnekortet Oppdateringer Andre Forrige registeroppdatering: %s aldri - Automatisk registeroppdatering - Oppdater applikasjonsliste fra register automatisk Bare på trådløst - Oppdater applikasjoner automatisk kun på trådløst Varsle - Varsle når nye oppdateringer er tilgjengelige Oppdater historie - Dager for visning av nye/oppdaterte applikasjoner Søkeresultater Programdetaljer Program ikke funnet @@ -45,7 +37,6 @@ Lisensiert GNU GPLv3. Avbryt Velg register du vil fjerne Oppdater registrene - Installert Tilgjengelig Oppdateringer 1 oppdatering tilgjengelig. @@ -85,17 +76,12 @@ Lisensiert GNU GPLv3. Dette programmet avhenger av andre ufrie applikasjoner Vis Ekspert - Skru på ekspertmodus Søk i programliste Modus for databasesynkronisering - Sett verdien for SQLites \"synchronous\"-flagg Programstøtte Ukompatible versjoner - Vis versjoner av applikasjoner som ikke er kompatible med ditt utstyr Rot - Vis applikasjoner som krever rottilgang Ignorer pekeskjerm - Inkluder alltid applikasjoner som krever pekeskjerm Alle Det som er nytt Nylig oppdatert @@ -111,10 +97,7 @@ Lisensiert GNU GPLv3. Krever ingen tillatelser. Tillatelser for versjon %s Vis tillatelser - Vis liste over tillatelser en applikasjon trenger Du har ingen tilgjengelige applikasjoner som kan håndtere %s Kompakt layout - Vis kun navn og sammendrag på applikasjoner i listen Utseende - Velg et utseende diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index 3c7c5bf94..6eeb3910a 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -7,22 +7,14 @@ Dit pakket is niet verenigbaar met uw apparaat. Wilt u het alsnog proberen te installeren? U probeert deze applicatie te degraderen naar een oudere versie. Als u dit doet uw data kan corrupt of verloren raken. Wilt u dit alsnog uitvoeren? Versie - %d versies beschikbaar - %d versie beschikbaar buffer gedownloade apps - Bewaar gedownloade apk-bestanden op de SD-kaart Updates Andere Laatste bronnen scan: %s nooit - Scan bronnen automatisch - app-lijst automatisch bijwerken Allen op wifi - Vernieuw app-lijst alleen op wifi automatisch Verwittigen - Verwittigen wanneer nieuwe updates beschikbaar zijn Vernieuw historie - Dagen om nieuwe/verbeterde apps te laten zien Zoekresultaten App Details Geen dergelijke app gevonden @@ -52,7 +44,6 @@ Een bron-adres ziet er ongeveer Annuleren Kies bron om te verwijderen Vernieuw bronnen - Geïnstalleerd Beschikbaar Updates 1 vernieuwing is beschikbaar @@ -93,17 +84,12 @@ Wilt u ze vernieuwen? Deze app vereist andere niet-vrije apps Laat zien Expert - Ga in expert-modus Zoek applicaties Database sync-modus - Zet de waarde van SQLite\'s synchronisatie-vlag Applicatie verenigbaarheid Onverenigbare versies - Laat versies van apps die onverenigbaar zijn met het apparaat Root - Laat apps zien die root-privileges vereisen Negeer aanraakscherm - Altijd apps die aanraakscherm vereisen weergeven Alles Wat is nieuw Recentelijk vernieuwd @@ -119,10 +105,7 @@ Wilt u ze vernieuwen? Geen permissies worden gebruikt Permissies voor versie %s Laat permissies zien - Laat een lijst met benodigde permissies van de app zien U hebt geen beschikbare app die %s kan verwerken Compacte Layout - Laat alleen namen en samenvattingen van apps zien in de lijst Thema - Kies een thema om te gebruiken diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index a4cd61bac..383525464 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -6,20 +6,13 @@ Nowa wersja jest podpisana innym kluczem niż poprzednia. Aby ją zainstalować należy najpierw usunąć tę starą. Zrób to i spróbuj ponownie. (Proszę pamiętać, że deinstalacja spowoduje usunięcie wszystkich danych przechowywanych przez aplikację) Wygląda na to, że ta aplikacja nie jest kompatybilna z twoim urządzeniem. Spróbować mimo to? Wersja - %d dostępnych wersji - %d dostępna wersja Buforuj pobrane aplikacje - Przechowuj pobrane pliki apk na karcie SD Aktualizacje Inne Ostatnie uaktualnienie listy aplikacji: %s nigdy - Automatycznie skanuj repozytoria - Automatycznie uaktualnij listę aplikacji z repozytorium Tylko przez wifi - Aktualizuj automatycznie tylko przez wifi Powiadom - Powiadamiaj, gdy dostępne będą nowe aktualizacje Historia aktualizacji Wyniki wyszukiwania Nie znaleziono takiej aplikacji @@ -43,7 +36,6 @@ Wydany na licencji GNU GPLv3. Nadpisz Wybierz repozytorium które chcesz usunąć Aktualizuj repozytoria - Zainstalowano Dostępne Aktualizacje Dostępne jest 1 uaktualnienie. @@ -85,13 +77,10 @@ Czy chcesz je zaktualizować? Ta aplikacja promuje niewolne usługi Ta aplikacja wymaga innych, niewolnych aplikacji Ekspert - Uruchom tryb eksperta Wyszukaj aplikacje Tryb synchronizacji bazy danych - Ustaw synchronizację flagi SQLite Kompatybilność aplikacji Root - Pokaż aplikacje wymagające uprawnień root Wszystkie Co nowego Ostatnio zaktualizowane @@ -102,9 +91,6 @@ Czy chcesz je zaktualizować? Sprawdzanie kompatybilności aplikacji z urządzeniem… Uprawnienia dla wersji %s Wyświetl uprawnienia - Wyświetlaj listę uprawnień wymaganych przez aplikację Widok kompaktowy - Wyświetlaj tylko nazwy i podsumowania aplikacji Motyw - Wybierz motyw diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index 22909f99e..c51a79245 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -7,22 +7,14 @@ Aparentemente esse pacote não é compatível com o seu dispositivo. Quer tentar instalá-lo mesmo assim? Você está tentando desatualizar este aplicativo. Isso pode causar mal funcionamento e eventualmente perda de dados. Você quer tentar desatualizá-lo mesmo assim? Versão - %d versões disponíveis - %d versão disponível Cache de aplicativos baixado - Manter no cartão SD os arquivos apk baixados Atualizações Outro Última consulta aos repositórios: %s nunca - Consulta automática aos repositórios - Atualizar a lista de aplicativos automaticamente a partir dos repositórios Só com wifi - Atualizar a lista de aplicativos somente via wifi Notificar - Notificar quando novas atualizações estiverem disponíveis Atualizar histórico - Dias para mostrar apps novos/atualizados Resultados da Pesquisa Detalhes do Aplicativo Nenhum aplicativo encontrado @@ -49,7 +41,6 @@ Um endereço do repositório é algo similar a isto: http://f-droid.org/repoCancelar Escolha o repositório para remover Atualizar repositórios - Instalado Disponível Atualizações 1 atualização disponível. @@ -90,17 +81,12 @@ Você deseja atualizá-los? Este aplicativo depende de aplicativos não-livres Exibição Especialista - Ativar modo especialista Pesquisar aplicativos Modo de sincronia do banco de dados - Definir o valor da flag de sincronia do SQLite Compatibilidade de aplicativo Versões incompatíveis - Mostrar versões de aplicativos incompatíveis com o dispositivo Root - Mostrar aplicativos que requerem privilégios de root Ignorar tela sensível ao toque - Sempre incluir aplicativos que requerem tela sensível ao toque Todos O que há de novo Atualizado Recentemente @@ -116,10 +102,7 @@ Você deseja atualizá-los? Nenhuma permissão utilizada. Permissões para a versão %s Mostrar permissões - Mostrar uma lista de permissões que um aplicativo requer Você não tem aplicativo instalado que lide com %s Leiaute compacto - Mostrar só nomes de aplicativos e sumários na lista Tema - Escolha que tema utilizar diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml index 6c2a501b3..40c1a4fe3 100644 --- a/res/values-ro/strings.xml +++ b/res/values-ro/strings.xml @@ -4,15 +4,9 @@ Sa gasit o aplicatie potrivita cu %s\' Nu exita aplicatii potrivite cu %s\': Versiune - Versiunile %d disponibile - Versiunea %d disponibila Istoric aplicatii descarcate - Patrati fisierele apk descarcate pe cardul SD Noutati - Scanare versiuni noi - Actualizare aplicatie automata Notificare - Notificare cand exista versiuni noi Despre F-Droid Bazat pe Aptoide. Distribuit sub licenta GNU GPLv3. @@ -29,7 +23,6 @@ Distribuit sub licenta GNU GPLv3. Anuleaza Alegeti depozitul pentru stergere Actualizare depozit aplicatii - Instalat Disponibil Actualizare Asteptati … diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index b603fcb90..225e618e9 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -6,19 +6,12 @@ Новая версия подписана ключом отличным от старого. Для установки новой версии, сначала нужно удалить старую программы. А потом попробовать снова. (Замечание: при удалении программы будут удалены все её данные) Вы пытаетесь установить более старую версию приложения. Это может привести к его некорректной работе и даже потере данных. Вы уверены, что хотите продолжить? Версия - версий доступно - %d - %d версия доступна Кешировать загруженные приложения - Сохранять загруженные apk файлы на SD карте Обновления Обновлено: %s никогда - Автоматически сканировать репозиторий - Обновлять список приложений автоматически Уведомление - Сообщать при появлении обновлений История обновлений - Сколько дней показывать новые/обновлённый приложения Результаты поиска Описание приложения Приложение не найдено @@ -45,7 +38,6 @@ Отменить Удалить репозиторий Обновить репозитории - Установлено Доступно Обновления Доступно 1 обновление. @@ -77,15 +69,11 @@ Загрузка остановлена Вид Эксперт - Включить режим эксперта Найти приложения Режим синхронизации базы - Установить флаг синхронизации SQLite Совместимость приложений Суперпользователь - Показывать приложения требующие root-привилегий Игнорировать Тачскрин - Всегда включать приложения требующие тачскрин Все Что Нового Недавно обновлённые @@ -98,7 +86,5 @@ Разрешений не требуется. Разрешения для версии %s Показывать разрешения - Показывать список разрешений, необходимых приложению Компактный вид - Показывать в списке только названия и краткие описания приложений diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index 041c75ec1..304697388 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -5,15 +5,9 @@ Nobena aplikacija ne ustreza \'%s\' Nova različica je overovljena z drugim ključem kot starejša. V primeru, da želite namestiti novejšo različico morate najprej odstraniti staro. Poskusite ponovno, ko ste to naredili. (Bodite pozorni na dejstvo, da bodo zaradi odstranitve izbrisani vsi notranji podatki shranjeni v aplikaciji) Različica - %d različic na razpolago - %d različica na razpolago Predpomnilnik naloženih aplikacij - Shrani naložene datoteke apk na kartico SD Posodobitve - Samodejni pregled skladišč - Samodejno posodobi spisek aplikacij iz skladišč Opozorilo - Opozori na posodobitve Päivityshistoria Izvorno osnovan na Aptoide. Izdan z licenco GNU GPLv3. @@ -31,7 +25,6 @@ Izdan z licenco GNU GPLv3. Prekliči Odstrani skladišče Posodobi skladišča - Nameščeno Na razpolago Posodobitve Počakajte prosim @@ -58,14 +51,10 @@ Ga želite posodobiti? Prenos je preklican Tämä ohjelma sisältää mainontaa Napredno - Vključi napredni način Iskanje aplikacij Način sinhronizacije baze podatkov - Nastavitev zastavice za sinhronost v SQLite Združljivost aplikacij Yhteensopimattomat versiot - . Skrbnik - Pokaži aplikacije, ki zahtevajo skrbniške pravice . diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index fb2d511b5..bb7a4516e 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -7,22 +7,14 @@ Изгледа да овај пакет није компатибилан са вашим уређајем. Да ли желите да га све једно инсталирате? Тренутно покушавате да инсталирате старију верзију ове апликације. То може да доведе до кварова и губитка података. Да ли сте сигурни да желите да инсталирате старију верзију? Верзија - %d верзије/верзија на располагању - %d верзија на располагању Чувај скинуте апликације - Чувај скинуте apk датотеке на СД картици Ажурирање Druga Задње скенирање ризнице: %s никада - Аутоматско скенирање ризница - Аутоматски ажурирај листу апликација Само на бежичној мрежи - Аутоматски ажурирај листе апликација само на бежичној мрежи Обавести - Обавести кад су нове верзије на располагању Претходна ажурирања - Колико дана приказивати нове/ажуриране апликације Резултати Претраге Детаљни подаци за Апликацију Та апликација не постоји @@ -49,7 +41,6 @@ Поништи Изабери ризницу за уклањање Ажурирај ризнице - Инсталиране На располагању Нове верзије 1 нова верзија на располагању. @@ -89,15 +80,11 @@ За ову апликацију су потребни плаћени додаци Прикажи Стручни - Омогући стручни режим Претрага апликација Режим синхронизације базе података - Унесите вредност за SQLite синхрону заставу Компатибилност апликације Рут - Приказати апликације које захтевају рут привилегије Игнориши Додирни Екран - Увек приказати апликације које захтевају додирни екран Све Ново Недавно Ажурирано @@ -113,8 +100,6 @@ Не захтевају се никакве дозволе. Дозволе за верзију %s Прикажи дозволе - Приказати листу дозвола неопходних за апликацију Немате инсталирану апликацију за %s Компактни Распоред - Само приказати имена и сажете описе апликација на лист diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index c3f9e8390..49eda1b32 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -7,22 +7,14 @@ Det verkar som att detta program inte är kompatibelt med enheten. Vill ni försöka installera det ändå? Du försöker nedgradera detta program. Detta kan få det att fungera felaktigt eller orsaka förlust av dina data. Vill du ändå försöka nedgradera? Version - %d versioner tillgängliga - %d version tillgänglig Cacha nerladdade appar - Behåll nerladdade apk-filer på SD-kortet Uppdateringar Andra Senaste förrådsavsökning: %s aldrig - Automatisk förrådsavsökning - Uppdatera applistan från förråd automatiskt Endast via WiFi - Uppdatera applistor automatiskt endast över wifi Avisering - Meddela mig när nya uppdateringar finns Uppdateringshistorik - Antal dagar att visa nya/uppdaterade appar Sökresultat Appdetaljer Ingen sådan app funnen @@ -52,7 +44,6 @@ En förrådsadress ser ut så här: https://f-droid.org/repo Skriv över Välj förråd att ta bort Uppdatera förråd - Installerade Tillgängliga Uppdateringar 1 uppdatering finns tillgänglig. @@ -100,17 +91,12 @@ Vill du uppdatera dem? Källkoden från uppströms är inte fullständigt fri Visning Expert - Aktivera expertläge Sök program Databassynkroniseringsläge - Ställ in värdet på synchronous-flaggan i SQLite Programkompatibilitet Inkompatibla versioner - Visa versioner av appar som är inkompatibla med enheten Root - Visa appar som kräver root-rättigheter Ignorera touchscreen - Inkludera alltid appar som kräver touchscreen Alla Nyheter Nyligt uppdaterade @@ -126,10 +112,7 @@ Vill du uppdatera dem? Inga behörigheter används. Behörigheter för version %s Visa behörigheter - Visa en lista över behörigheter en app behöver Du har inte någon app tillgänglig för hantering av %s Kompakt layout - Visa endast appnamn och sammanfattningar i listan Tema - Välj ett tema att använda diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml index c4698caf7..ab4e6a6c8 100644 --- a/res/values-tr/strings.xml +++ b/res/values-tr/strings.xml @@ -7,22 +7,14 @@ Bu paket cihazınızla uyumlu değil gibi görünüyor. Yine de kurmayı denemek istiyor musunuz? Bu uygulamanın eski bir sürümüne dönmek üzeresiniz. Bu, uygulamanın yanlış çalışmasına ve hatta veri kaybına neden olabilir. Devam etmek istiyor musunuz? Sürüm - %d sürüm mevcut - %d sürüm mevcut İndirilen uygulamaları önbelleğe kaydet - İndirilen uygulamaları SD kartına kaydet Güncellemeler Diğer Son depo analizi: %s asla - Otomatik depo taraması - Uygulama listesini depolardan otomatik olarak güncelle Sadece WiFi ile - Uygulama listesini otomatik olarak sadece WiFi ile güncelle Bildirme - Yeni güncellemeler olduğunu bildir Güncelleme tarihçesi - Yeni/güncellenmiş uygulamaların gösterilecekleri gün sayısı Arama Sonuçları Uygulama Detayları Böyle bir uygulama bulunamadı @@ -49,7 +41,6 @@ Bir depo adresi şuna benzer: https://f-droid.org/repo İptal Kaldırılacak depoyu seç Depoları güncelle - Kurulu Mevcut Güncellemeler 1 güncelleme bulunmaktadır. @@ -90,17 +81,12 @@ Güncellemek ister misiniz? Bu uygulama özgür olmayan başka uygulamalara bağımlıdır Görüntüleme Uzman - Uzman modunu etkinleştir Uygulama ara Veritabanı eşleşme modu - SQLite\'ın senkronize flag değerini gir Uygulama uyumu Uyumsuz sürümler - Cihazınızla uyumsuz uygulama sürümlerini göster Root - Root yetkilerine gerek duyan uygulamaları göster Dokunmatik ekranı yok say - Dokunmatik ekran gerektiren uygulamaları daima ekle Tümü Yeni olanlar Yakın geçmişte güncellenen @@ -116,10 +102,7 @@ bağlanılıyor Hiçbir izin kullanılmıyor. %s sürümü için izinler İzinleri göster - Uygulamanın gerektirdiği izinlerin listesini göster %s unsurunu yönetecek hiçbir mevcut uygulamanız yok Yoğun düzen - Listede sadece uygulama adlarını ve özetleri göster Tema - Kullanılacak temayı seç diff --git a/res/values-ug/strings.xml b/res/values-ug/strings.xml index 612a5ab4a..ffb278803 100644 --- a/res/values-ug/strings.xml +++ b/res/values-ug/strings.xml @@ -7,22 +7,14 @@ بۇ بوغچا ئۈسكۈنىڭىز بىلەن ماسلاشمايدىغاندەك تۇرىدۇ، ئۇنى سىناپ ئورنىتىۋېرەمسىز؟ بۇ ئەپنىڭ دەرىجىسىنى تۆۋەنلىتىشنى سىناۋاتىسىز. بۇ مەشغۇلاتنى ئىجرا قىلىش داۋامىدا كاشىلا كۆرۈلۈشى ۋە سانلىق مەلۇماتلىرىڭىزنى يوقۇتۇپ قويۇشىڭىز مۇمكىن. ئۇنى سىناپ دەرىجىسىنى تۆۋەنلىتىۋېرەمسىز؟ نەشرى - %d نەشرى بار - %d نەشرى بار ئەپلەر غەملەككە چۈشۈرۈلدى - چۈشۈرگەن apk ھۆججەتلەرنى SD كارتىدا ساقلاپ قال يېڭىلانمىلار باشقا ئاخىرقى repo تەكشۈرۈش: %s ھەرگىز - ئاپتوماتىك repo تەكشۈرۈش - ئەپ تىزىمىنى خەزىنەدىن ئۆزلۈكىدىن يېڭىلا wifi دىلا - ئەپ تىزىمىنى wifi دىلا ئۆزلۈكىدىن يېڭىلا ئۇقتۇرۇش - يېڭى يېڭىلانمىلار بولسا ئەسكەرت يېڭىلاش تارىخى - يېڭى/يېڭىلانغان ئەپلەرنى كۆرسىتىدىغان كۈن سانى ئىزدەش نەتىجىلىرى ئەپ تەپسىلاتلىرى بۇنداق ئەپ تېپىلمىدى @@ -49,7 +41,6 @@ ۋاز كەچ چىقىرىۋېتىدىغان خەزىنەنى تاللاڭ خەزىنە يېڭىلا - ئورنىتىلغان ئىشلىتىشچان يېڭىلانمىلار 1 يېڭىلانما بار. @@ -90,17 +81,12 @@ بۇ ئەپ ھەقسىز بولمىغان باشقا ئەپلەرگە بېقىنىدۇ كۆرسەت ئالىي - ئالىي ھالەتنى قوزغات ئەپ ئىزدە ساندان قەدەمداش ھالەت - بۇ SQLite قەدەمداش بايراقىنىڭ قىممىتىنى تەڭشەيدۇ ئەپ ماسلىشىشچانلىقى ماسلاشمايدىغان نەشرىلىرى - ئەپلەرنىڭ ئۈسكۈنە بىلەن ماسلاشمايدىغان نەشرىلىرىنى كۆرسىتىدۇ Root - root ھوقۇقى زۆرۈر بولغان ئەپلەرنى كۆرسەت سەزگۈر ئېكرانغا پەرۋا قىلما - ھەمىشە سەزگۈر ئېكرانلىق ئەپلەرنى ئۆز ئىچىگە ئالىدۇ ھەممىسى يېڭىلىقلار يېقىنقى يېڭىلانغانلار @@ -116,10 +102,7 @@ ھېچقانداق ھوقۇق ئىشلەتمەيدۇ. %s نەشرىنىڭ ھوقۇقلىرى ھوقۇقلارنى كۆرسەت - ئەپكە زۆرۈر بولغان ھوقۇق تىزىمىنى كۆرسىتىدۇ سىز %s نى بىر تەرەپ قىلالايدىغان ھېچقانداق ئەپ ئورناتمىغان ئىخچام جايلاشتۇرۇش - تىزىمدا پەقەت ئەپ ئىسمى ۋە ئۈزۈندىلىرىنىلا كۆرسىتىدۇ ئۆرنەك - ئىشلىتىدىغان ئۆرنەكتىن بىرنى تاللاڭ diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index 6bfa64745..4ea3a11c4 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -5,17 +5,11 @@ Не знайдено програм за запитом «%2$s». Нова версія підписана не тим ключем, що стара. Перш ніж встановити нову версію, самостійно зітріть стару. Зауважте, що стирання програми призведе до знищення всіх даних цієї програми. Версія - Наявно версій: %d - Наявна %d версія Зберігати звантажене - Зберігати звантажені APK-файли на карті пам’яті Оновлення Синхронізовано: %s ніколи - Синхронізація - Автоматично оновлювати список програм із репозиторію Сповіщення - Сповіщати про наявність оновлень Про F-Droid Сайт: Пошта: @@ -31,7 +25,6 @@ Назад Видалити репозиторій Оновити репозиторії? - Встановлене Наявне Оновлення Зачекайте @@ -59,15 +52,11 @@ Отриманий файл пошкоджений Звантаження скасовано Експерт - Увімкнути режим експерта Пошук програм Синхронізація БД - Режим синхронізації SQLite Сумісність Суперкористувач - Показувати програми, для яких потрібні права суперкористувача Ігнорувати тачскрін - Завжди показувати програми, які потребують тачскрін Всі програми Недавні додання Недавні оновлення diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index b7cd6f111..80451bdda 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -5,17 +5,11 @@ 没有找到 \'%s\'相关内容 新版本签名与旧版本不同,请先卸载旧版本应用再安装新版本。(注意:卸载旧版本会清除该应用的所有已储存数据) 版本 - %d个可用版本 - %d个可用版本 已下载应用缓存 - 在SD卡中保留下载的apk文件 升级 最后一次repo扫描: 从不 - 自动扫描repo - 自动更新应用列表 通知 - 当有更新时,通知栏提醒 关于F-Droid 网站: 邮件: @@ -31,7 +25,6 @@ 取消 选择要移除的应用源 更新应用源 - 已经安装的 可安装 更新 请等一下 @@ -59,15 +52,11 @@ 文件下载错误 下载取消 高级 - 开启高级模式 搜索应用 数据同步模式 - 设置 SQLite\'s synchronous flag的值 应用兼容性 Root - 显示需要root权限的应用 忽略需要触屏的应用 - 总是显示需要触屏的应用 全部 新鲜货 最近更新 From a01bcd6be23559da7438a747b629f93e3ab8394a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 29 Jan 2014 23:44:49 +0100 Subject: [PATCH 058/282] Don't use find in fix-ellipsis --- tools/fix-ellipsis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/fix-ellipsis.sh b/tools/fix-ellipsis.sh index a00e26859..b3f02fb85 100755 --- a/tools/fix-ellipsis.sh +++ b/tools/fix-ellipsis.sh @@ -2,4 +2,4 @@ # Fix TypographyEllipsis programmatically -find res/values* -name '*.xml' -type f | xargs -n 1 sed -i 's/\.\.\./…/g' +sed -i 's/\.\.\./…/g' res/values*/*.xml From 2a8c570a00f28d0b45cb9e11fd894e67f21a5c62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 29 Jan 2014 23:45:42 +0100 Subject: [PATCH 059/282] Release 0.59-test --- AndroidManifest.xml | 4 ++-- res/values/no_trans.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 44248cfcf..c8b373eb7 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="590" + android:versionName="0.59-test" > F-Droid - 0.58 + 0.59-test https://f-droid.org team@f-droid.org From 3b5509ff4be8e48a0798859632ac776f7f3a227c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 30 Jan 2014 17:01:18 +0100 Subject: [PATCH 060/282] Fix changelog markdown --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f5fab50c..1af5edcf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ * Fixed a crash when trying to access a non-existing app -* F-Droid registers with Android to receive F-Droid URIs https://*/fdroid/repo +* F-Droid registers with Android to receive F-Droid URIs https://\*/fdroid/repo and fdroidrepos:// * support including signing key fingerprint in repo URIs From b3773a156121cfce94aa6db769c7c7ff5fb2913d Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 31 Jan 2014 03:15:48 +1100 Subject: [PATCH 061/282] Refactoring Apk references into content provider. Removed DB.Apk in favour of stand-alone Apk class. Conflicts: src/org/fdroid/fdroid/DB.java --- AndroidManifest.xml | 7 +- TODO | 3 + src/org/fdroid/fdroid/AppDetails.java | 21 +- src/org/fdroid/fdroid/DB.java | 297 ++------------ src/org/fdroid/fdroid/Downloader.java | 11 +- src/org/fdroid/fdroid/RepoXMLHandler.java | 5 +- src/org/fdroid/fdroid/UpdateService.java | 5 +- src/org/fdroid/fdroid/data/Apk.java | 216 ++++++++++ src/org/fdroid/fdroid/data/ApkProvider.java | 370 ++++++++++++++++++ src/org/fdroid/fdroid/data/DBHelper.java | 40 +- src/org/fdroid/fdroid/data/Repo.java | 18 +- src/org/fdroid/fdroid/data/RepoProvider.java | 27 +- src/org/fdroid/fdroid/data/ValueObject.java | 23 ++ .../views/fragments/RepoDetailsFragment.java | 5 +- 14 files changed, 729 insertions(+), 319 deletions(-) create mode 100644 TODO create mode 100644 src/org/fdroid/fdroid/data/Apk.java create mode 100644 src/org/fdroid/fdroid/data/ApkProvider.java create mode 100644 src/org/fdroid/fdroid/data/ValueObject.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c8b373eb7..248240f6c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -36,10 +36,15 @@ android:supportsRtl="false" > + + diff --git a/TODO b/TODO new file mode 100644 index 000000000..63f46645f --- /dev/null +++ b/TODO @@ -0,0 +1,3 @@ +incompatible_reasons needs to be implemented correctly for the data.Apk class (rather than DB.Apk). +Currently, it is set during a CompatabilityChecker call to isCompatible(), which means we don't really +know whether the field has been set or not. diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index c01eccdb5..d39486d8a 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; import org.xml.sax.XMLReader; @@ -74,13 +75,9 @@ import org.fdroid.fdroid.compat.ActionBarCompat; import org.fdroid.fdroid.compat.MenuManager; import org.fdroid.fdroid.DB.CommaSeparatedList; -import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.ImageScaleType; -import com.nostra13.universalimageloader.utils.StorageUtils; - -import android.os.Environment; public class AppDetails extends ListActivity { @@ -99,13 +96,13 @@ public class AppDetails extends ListActivity { private class ApkListAdapter extends BaseAdapter { - private List items; + private List items; private LayoutInflater mInflater; - public ApkListAdapter(Context context, List items) { - this.items = new ArrayList(); + public ApkListAdapter(Context context, List items) { + this.items = new ArrayList(); if (items != null) { - for (DB.Apk apk : items) { + for (Apk apk : items) { this.addItem(apk); } } @@ -113,13 +110,13 @@ public class AppDetails extends ListActivity { Context.LAYOUT_INFLATER_SERVICE); } - public void addItem(DB.Apk apk) { + public void addItem(Apk apk) { if (apk.compatible || pref_incompatibleVersions) { items.add(apk); } } - public List getItems() { + public List getItems() { return items; } @@ -142,7 +139,7 @@ public class AppDetails extends ListActivity { public View getView(int position, View convertView, ViewGroup parent) { java.text.DateFormat df = DateFormat.getDateFormat(mctx); - DB.Apk apk = items.get(position); + Apk apk = items.get(position); ViewHolder holder; if (convertView == null) { @@ -983,7 +980,7 @@ public class AppDetails extends ListActivity { private boolean updating; private String id; - public DownloadHandler(DB.Apk apk, String repoaddress, File destdir) { + public DownloadHandler(Apk apk, String repoaddress, File destdir) { id = apk.id; download = new Downloader(apk, repoaddress, destdir); download.start(); diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index e94374bab..b101cad14 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -21,16 +21,17 @@ package org.fdroid.fdroid; import java.io.File; import java.security.MessageDigest; +import java.net.MalformedURLException; +import java.net.URL; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.Formatter; import java.util.HashMap; -import java.util.Set; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -55,8 +56,7 @@ import android.util.Log; import org.fdroid.fdroid.compat.Compatibility; import org.fdroid.fdroid.compat.ContextCompat; import org.fdroid.fdroid.compat.SupportedArchitectures; -import org.fdroid.fdroid.data.DBHelper; -import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.*; public class DB { @@ -270,155 +270,6 @@ public class DB { } - // The TABLE_APK table stores details of all the application versions we - // know about. Each relates directly back to an entry in TABLE_APP. - // This information is retrieved from the repositories. - public static final String TABLE_APK = "fdroid_apk"; - - public static class Apk { - - public Apk() { - updated = false; - detail_size = 0; - added = null; - repo = 0; - detail_hash = null; - detail_hashType = null; - detail_permissions = null; - compatible = false; - } - - public String id; - public String version; - public int vercode; - public int detail_size; // Size in bytes - 0 means we don't know! - public long repo; // ID of the repo it comes from - public String detail_hash; - public String detail_hashType; - public int minSdkVersion; // 0 if unknown - public Date added; - public CommaSeparatedList detail_permissions; // null if empty or - // unknown - public CommaSeparatedList features; // null if empty or unknown - - public CommaSeparatedList nativecode; // null if empty or unknown - - public CommaSeparatedList incompatible_reasons; // null if empty or - // unknown - // ID (md5 sum of public key) of signature. Might be null, in the - // transition to this field existing. - public String sig; - - // True if compatible with the device. - public boolean compatible; - - public String apkName; - - // If not null, this is the name of the source tarball for the - // application. Null indicates that it's a developer's binary - // build - otherwise it's built from source. - public String srcname; - - // Used internally for tracking during repo updates. - public boolean updated; - - // Call isCompatible(apk) on an instance of this class to - // check if an APK is compatible with the user's device. - private static class CompatibilityChecker extends Compatibility { - - private Set features; - private Set cpuAbis; - private String cpuAbisDesc; - private boolean ignoreTouchscreen; - - public CompatibilityChecker(Context ctx) { - - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(ctx); - ignoreTouchscreen = prefs - .getBoolean(Preferences.PREF_IGN_TOUCH, false); - - PackageManager pm = ctx.getPackageManager(); - StringBuilder logMsg = new StringBuilder(); - logMsg.append("Available device features:"); - features = new HashSet(); - if (pm != null) { - for (FeatureInfo fi : pm.getSystemAvailableFeatures()) { - features.add(fi.name); - logMsg.append('\n'); - logMsg.append(fi.name); - } - } - - cpuAbis = SupportedArchitectures.getAbis(); - - StringBuilder builder = new StringBuilder(); - boolean first = true; - for (String abi : cpuAbis) { - if (first) first = false; - else builder.append(", "); - builder.append(abi); - } - cpuAbisDesc = builder.toString(); - builder = null; - - Log.d("FDroid", logMsg.toString()); - } - - private boolean compatibleApi(CommaSeparatedList nativecode) { - if (nativecode == null) return true; - for (String abi : nativecode) { - if (cpuAbis.contains(abi)) { - return true; - } - } - return false; - } - - public boolean isCompatible(Apk apk) { - if (!hasApi(apk.minSdkVersion)) { - apk.incompatible_reasons = CommaSeparatedList.make(String.valueOf(apk.minSdkVersion)); - return false; - } - if (apk.features != null) { - for (String feat : apk.features) { - if (ignoreTouchscreen - && feat.equals("android.hardware.touchscreen")) { - // Don't check it! - } else if (!features.contains(feat)) { - apk.incompatible_reasons = CommaSeparatedList.make(feat); - Log.d("FDroid", apk.id + " vercode " + apk.vercode - + " is incompatible based on lack of " - + feat); - return false; - } - } - } - if (!compatibleApi(apk.nativecode)) { - apk.incompatible_reasons = apk.nativecode; - Log.d("FDroid", apk.id + " vercode " + apk.vercode - + " only supports " + CommaSeparatedList.str(apk.nativecode) - + " while your architectures are " + cpuAbisDesc); - return false; - } - return true; - } - } - } - - public int countAppsForRepo(long id) { - String[] selection = { "COUNT(distinct id)" }; - String[] selectionArgs = { Long.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; - } - } - public static String calcFingerprint(String keyHexString) { if (TextUtils.isEmpty(keyHexString)) return null; @@ -569,33 +420,21 @@ public class DB { } } - private static final String[] POPULATE_APK_COLS = new String[] { "hash", "hashType", "size", "permissions" }; + private static final String[] POPULATE_APK_COLS = new String[] { + ApkProvider.DataColumns.HASH, + ApkProvider.DataColumns.HASH_TYPE, + ApkProvider.DataColumns.SIZE, + ApkProvider.DataColumns.PERMISSIONS + }; private void populateApkDetails(Apk apk, long repo) { if (repo == 0 || repo == apk.repo) { - Cursor cursor = null; - try { - cursor = db.query( - TABLE_APK, - POPULATE_APK_COLS, - "id = ? and vercode = ?", - new String[] { apk.id, - Integer.toString(apk.vercode) }, null, - null, null, null); - cursor.moveToFirst(); - apk.detail_hash = cursor.getString(0); - apk.detail_hashType = cursor.getString(1); - apk.detail_size = cursor.getInt(2); - apk.detail_permissions = CommaSeparatedList.make(cursor - .getString(3)); - } catch (Exception e) { - Log.d("FDroid", "Error populating apk details for " + apk.id + " (version " + apk.version + ")"); - Log.d("FDroid", e.getMessage()); - } finally { - if (cursor != null) { - cursor.close(); - } - } + Apk loadedApk = ApkProvider.Helper.find( + mContext, apk.id, apk.vercode, POPULATE_APK_COLS); + apk.detail_hash = loadedApk.detail_hash; + apk.detail_hashType = loadedApk.detail_hashType; + apk.detail_size = loadedApk.detail_size; + apk.detail_permissions = loadedApk.detail_permissions; } else { Log.d("FDroid", "Not setting details for apk '" + apk.id + "' (version " + apk.version +") because it belongs to a different repo."); } @@ -695,18 +534,6 @@ public class DB { Log.d("FDroid", "Read app data from database " + " (took " + (System.currentTimeMillis() - startTime) + " ms)"); - String query = "SELECT apk.id, apk.version, apk.vercode, apk.sig," - + " apk.srcname, apk.apkName, apk.minSdkVersion, " - + " apk.added, apk.features, apk.nativecode, " - + " apk.compatible, apk.repo, repo.version, repo.address " - + " FROM " + TABLE_APK + " as apk " - + " LEFT JOIN " + DBHelper.TABLE_REPO + " as repo " - + " ON repo._id = apk.repo " - + " ORDER BY apk.vercode DESC"; - - c = db.rawQuery(query, null); - c.moveToFirst(); - DisplayMetrics metrics = mContext.getResources() .getDisplayMetrics(); String iconsDir = null; @@ -726,43 +553,21 @@ public class DB { metrics = null; Log.d("FDroid", "Density-specific icons dir is " + iconsDir); - while (!c.isAfterLast()) { - String id = c.getString(0); - App app = apps.get(id); + List apks = ApkProvider.Helper.all(mContext); + for (Apk apk : apks) { + App app = apps.get(apk.id); if (app == null) { - c.moveToNext(); continue; } - boolean compatible = c.getInt(10) == 1; - int repoid = c.getInt(11); - Apk apk = new Apk(); - apk.id = id; - apk.version = c.getString(1); - apk.vercode = c.getInt(2); - apk.sig = c.getString(3); - apk.srcname = c.getString(4); - apk.apkName = c.getString(5); - apk.minSdkVersion = c.getInt(6); - String sApkAdded = c.getString(7); - apk.added = (sApkAdded == null || sApkAdded.length() == 0) ? null - : DATE_FORMAT.parse(sApkAdded); - apk.features = CommaSeparatedList.make(c.getString(8)); - apk.nativecode = CommaSeparatedList.make(c.getString(9)); - apk.compatible = compatible; - apk.repo = repoid; app.apks.add(apk); if (app.iconUrl == null && app.icon != null) { - int repoVersion = c.getInt(12); - String repoAddress = c.getString(13); - if (repoVersion >= 11) { - app.iconUrl = repoAddress + iconsDir + app.icon; + if (apk.repoVersion >= 11) { + app.iconUrl = apk.repoAddress + iconsDir + app.icon; } else { - app.iconUrl = repoAddress + "/icons/" + app.icon; + app.iconUrl = apk.repoAddress + "/icons/" + app.icon; } } - c.moveToNext(); } - c.close(); } catch (Exception e) { Log.e("FDroid", @@ -948,8 +753,8 @@ public class DB { // in the repos. Log.d("FDroid", "AppUpdate: " + app.name + " is no longer in any repository - removing"); - db.delete(TABLE_APP, "id = ?", new String[] { app.id }); - db.delete(TABLE_APK, "id = ?", new String[] { app.id }); + db.delete(TABLE_APP, "id = ?", new String[]{app.id}); + ApkProvider.Helper.deleteApksByApp(mContext, app); } else { for (Apk apk : app.apks) { if (!apk.updated) { @@ -958,8 +763,7 @@ public class DB { Log.d("FDroid", "AppUpdate: Package " + apk.id + "/" + apk.version + " is no longer in any repository - removing"); - db.delete(TABLE_APK, "id = ? and version = ?", - new String[] { app.id, apk.version }); + ApkProvider.Helper.delete(mContext, app.id, apk.vercode); } } } @@ -1014,9 +818,13 @@ public class DB { boolean afound = false; for (Apk apk : app.apks) { if (apk.vercode == upapk.vercode) { - // Log.d("FDroid", "AppUpdate: " + apk.version - // + " is a known version."); - updateApkIfDifferent(apk, upapk); + + ApkProvider.Helper.update( + mContext, + upapk, + apk.id, + apk.vercode); + apk.updated = true; afound = true; break; @@ -1024,7 +832,7 @@ public class DB { } if (!afound) { // A new version of this application. - updateApkIfDifferent(null, upapk); + ApkProvider.Helper.insert(mContext, upapk); upapk.updated = true; app.apks.add(upapk); } @@ -1036,7 +844,7 @@ public class DB { // It's a brand new application... updateApp(null, upapp); for (Apk upapk : upapp.apks) { - updateApkIfDifferent(null, upapk); + ApkProvider.Helper.insert(mContext, upapk); upapk.updated = true; } upapp.updated = true; @@ -1095,41 +903,6 @@ public class DB { } } - // Update apk details in the database, if different to the - // previous ones. - // 'oldapk' - previous details - i.e. what's in the database. - // If null, this apk is not in the database at all and - // should be added. - // 'upapk' - updated details - private void updateApkIfDifferent(Apk oldapk, Apk upapk) { - ContentValues values = new ContentValues(); - values.put("id", upapk.id); - values.put("version", upapk.version); - values.put("vercode", upapk.vercode); - values.put("repo", upapk.repo); - values.put("hash", upapk.detail_hash); - values.put("hashType", upapk.detail_hashType); - values.put("sig", upapk.sig); - values.put("srcname", upapk.srcname); - values.put("size", upapk.detail_size); - values.put("apkName", upapk.apkName); - values.put("minSdkVersion", upapk.minSdkVersion); - values.put("added", - upapk.added == null ? "" : DATE_FORMAT.format(upapk.added)); - values.put("permissions", - CommaSeparatedList.str(upapk.detail_permissions)); - values.put("features", CommaSeparatedList.str(upapk.features)); - values.put("nativecode", CommaSeparatedList.str(upapk.nativecode)); - values.put("compatible", upapk.compatible ? 1 : 0); - if (oldapk != null) { - db.update(TABLE_APK, values, - "id = ? and vercode = ?", - new String[] { oldapk.id, Integer.toString(oldapk.vercode) }); - } else { - db.insert(TABLE_APK, null, values); - } - } - public void setIgnoreUpdates(String appid, boolean All, int This) { db.execSQL("update " + TABLE_APP + " set" + " ignoreAllUpdates=" + (All ? '1' : '0') @@ -1141,7 +914,7 @@ public class DB { db.beginTransaction(); try { - db.delete(TABLE_APK, "repo = ?", new String[] { Long.toString(repo.getId()) }); + ApkProvider.Helper.deleteApksByRepo(mContext, repo); List apps = getApps(false); for (App app : apps) { if (app.apks.isEmpty()) { diff --git a/src/org/fdroid/fdroid/Downloader.java b/src/org/fdroid/fdroid/Downloader.java index e0cb96ad6..ebd45271c 100644 --- a/src/org/fdroid/fdroid/Downloader.java +++ b/src/org/fdroid/fdroid/Downloader.java @@ -27,10 +27,11 @@ import java.io.OutputStream; import java.net.URL; import android.util.Log; +import org.fdroid.fdroid.data.Apk; public class Downloader extends Thread { - private DB.Apk curapk; + private Apk curapk; private String repoaddress; private String filename; private File destdir; @@ -38,11 +39,11 @@ public class Downloader extends Thread { public static enum Status { STARTING, RUNNING, ERROR, DONE, CANCELLED - }; + } public static enum Error { CORRUPT, UNKNOWN - }; + } private Status status = Status.STARTING; private Error error; @@ -52,7 +53,7 @@ public class Downloader extends Thread { // Constructor - creates a Downloader to download the given Apk, // which must have its detail populated. - Downloader(DB.Apk apk, String repoaddress, File destdir) { + Downloader(Apk apk, String repoaddress, File destdir) { curapk = apk; this.repoaddress = repoaddress; this.destdir = destdir; @@ -91,7 +92,7 @@ public class Downloader extends Thread { } // The APK being downloaded - public synchronized DB.Apk getApk() { + public synchronized Apk getApk() { return curapk; } diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java index 59a109b7a..a22c98c36 100644 --- a/src/org/fdroid/fdroid/RepoXMLHandler.java +++ b/src/org/fdroid/fdroid/RepoXMLHandler.java @@ -20,6 +20,7 @@ package org.fdroid.fdroid; import android.os.Bundle; +import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.updater.RepoUpdater; import org.xml.sax.Attributes; @@ -40,7 +41,7 @@ public class RepoXMLHandler extends DefaultHandler { private List appsList; private DB.App curapp = null; - private DB.Apk curapk = null; + private Apk curapk = null; private StringBuilder curchars = new StringBuilder(); // After processing the XML, these will be -1 if the index didn't specify @@ -280,7 +281,7 @@ public class RepoXMLHandler extends DefaultHandler { totalAppCount, progressData)); } else if (localName.equals("package") && curapp != null && curapk == null) { - curapk = new DB.Apk(); + curapk = new Apk(); curapk.id = curapp.id; curapk.repo = repo.getId(); hashType = null; diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index d161cc5ae..7d4063a88 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -49,6 +49,7 @@ import android.text.TextUtils; import android.util.Log; import android.widget.Toast; +import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.updater.RepoUpdater; @@ -351,7 +352,7 @@ public class UpdateService extends IntentService implements ProgressListener { for (long keep : keeprepos) { for (DB.App app : apps) { boolean keepapp = false; - for (DB.Apk apk : app.apks) { + for (Apk apk : app.apks) { if (apk.repo == keep) { keepapp = true; break; @@ -371,7 +372,7 @@ public class UpdateService extends IntentService implements ProgressListener { } app_k.updated = true; db.populateDetails(app_k, keep); - for (DB.Apk apk : app.apks) + for (Apk apk : app.apks) if (apk.repo == keep) apk.updated = true; } diff --git a/src/org/fdroid/fdroid/data/Apk.java b/src/org/fdroid/fdroid/data/Apk.java new file mode 100644 index 000000000..cf0c0a0f7 --- /dev/null +++ b/src/org/fdroid/fdroid/data/Apk.java @@ -0,0 +1,216 @@ +package org.fdroid.fdroid.data; + +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.FeatureInfo; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.preference.PreferenceManager; +import android.util.Log; +import org.fdroid.fdroid.DB; +import org.fdroid.fdroid.compat.Compatibility; +import org.fdroid.fdroid.compat.SupportedArchitectures; + +import java.util.Date; +import java.util.Set; +import java.util.HashSet; + +public class Apk { + + public String id; + public String version; + public int vercode; + public int detail_size; // Size in bytes - 0 means we don't know! + public long repo; // ID of the repo it comes from + public String detail_hash; + public String detail_hashType; + public int minSdkVersion; // 0 if unknown + public Date added; + public DB.CommaSeparatedList detail_permissions; // null if empty or + // unknown + public DB.CommaSeparatedList features; // null if empty or unknown + + public DB.CommaSeparatedList nativecode; // null if empty or unknown + + // ID (md5 sum of public key) of signature. Might be null, in the + // transition to this field existing. + public String sig; + + // True if compatible with the device. + public boolean compatible; + + public String apkName; + + // If not null, this is the name of the source tarball for the + // application. Null indicates that it's a developer's binary + // build - otherwise it's built from source. + public String srcname; + + // Used internally for tracking during repo updates. + public boolean updated; + + public int repoVersion; + public String repoAddress; + public DB.CommaSeparatedList incompatible_reasons; + + public Apk() { + updated = false; + detail_size = 0; + added = null; + repo = 0; + detail_hash = null; + detail_hashType = null; + detail_permissions = null; + compatible = false; + } + + public Apk(Cursor cursor) { + for(int i = 0; i < cursor.getColumnCount(); i ++ ) { + String column = cursor.getColumnName(i); + if (column.equals(ApkProvider.DataColumns.HASH)) { + detail_hash = cursor.getString(i); + } else if (column.equals(ApkProvider.DataColumns.HASH_TYPE)) { + detail_hashType = cursor.getString(i); + } else if (column.equals(ApkProvider.DataColumns.ADDED_DATE)) { + added = ValueObject.toDate(cursor.getString(i)); + } else if (column.equals(ApkProvider.DataColumns.FEATURES)) { + features = DB.CommaSeparatedList.make(cursor.getString(i)); + } else if (column.equals(ApkProvider.DataColumns.APK_ID)) { + id = cursor.getString(i); + } else if (column.equals(ApkProvider.DataColumns.IS_COMPATIBLE)) { + compatible = cursor.getInt(i) == 1; + } else if (column.equals(ApkProvider.DataColumns.MIN_SDK_VERSION)) { + minSdkVersion = cursor.getInt(i); + } else if (column.equals(ApkProvider.DataColumns.NAME)) { + apkName = cursor.getString(i); + } else if (column.equals(ApkProvider.DataColumns.PERMISSIONS)) { + detail_permissions = DB.CommaSeparatedList.make(cursor.getString(i)); + } else if (column.equals(ApkProvider.DataColumns.NATIVE_CODE)) { + nativecode = DB.CommaSeparatedList.make(cursor.getString(i)); + } else if (column.equals(ApkProvider.DataColumns.REPO_ID)) { + repo = cursor.getInt(i); + } else if (column.equals(ApkProvider.DataColumns.SIGNATURE)) { + sig = cursor.getString(i); + } else if (column.equals(ApkProvider.DataColumns.SIZE)) { + detail_size = cursor.getInt(i); + } else if (column.equals(ApkProvider.DataColumns.SOURCE_NAME)) { + srcname = cursor.getString(i); + } else if (column.equals(ApkProvider.DataColumns.VERSION)) { + version = cursor.getString(i); + } else if (column.equals(ApkProvider.DataColumns.VERSION_CODE)) { + vercode = cursor.getInt(i); + } else if (column.equals(ApkProvider.DataColumns.REPO_VERSION)) { + repoVersion = cursor.getInt(i); + } else if (column.equals(ApkProvider.DataColumns.REPO_ADDRESS)) { + repoAddress = cursor.getString(i); + } + } + } + + public ContentValues toContentValues() { + ContentValues values = new ContentValues(); + values.put(ApkProvider.DataColumns.APK_ID, id); + values.put(ApkProvider.DataColumns.VERSION, version); + values.put(ApkProvider.DataColumns.VERSION_CODE, vercode); + values.put(ApkProvider.DataColumns.REPO_ID, repo); + values.put(ApkProvider.DataColumns.HASH, detail_hash); + values.put(ApkProvider.DataColumns.HASH_TYPE, detail_hashType); + values.put(ApkProvider.DataColumns.SIGNATURE, sig); + values.put(ApkProvider.DataColumns.SOURCE_NAME, srcname); + values.put(ApkProvider.DataColumns.SIZE, detail_size); + values.put(ApkProvider.DataColumns.NAME, apkName); + values.put(ApkProvider.DataColumns.MIN_SDK_VERSION, minSdkVersion); + values.put(ApkProvider.DataColumns.ADDED_DATE, + added == null ? "" : DB.DATE_FORMAT.format(added)); + values.put(ApkProvider.DataColumns.PERMISSIONS, + DB.CommaSeparatedList.str(detail_permissions)); + values.put(ApkProvider.DataColumns.FEATURES, DB.CommaSeparatedList.str(features)); + values.put(ApkProvider.DataColumns.NATIVE_CODE, DB.CommaSeparatedList.str(nativecode)); + values.put(ApkProvider.DataColumns.IS_COMPATIBLE, compatible ? 1 : 0); + return values; + } + + // Call isCompatible(apk) on an instance of this class to + // check if an APK is compatible with the user's device. + public static class CompatibilityChecker extends Compatibility { + + private Set features; + private Set cpuAbis; + private String cpuAbisDesc; + private boolean ignoreTouchscreen; + + public CompatibilityChecker(Context ctx) { + + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(ctx); + ignoreTouchscreen = prefs + .getBoolean("ignoreTouchscreen", false); + + PackageManager pm = ctx.getPackageManager(); + StringBuilder logMsg = new StringBuilder(); + logMsg.append("Available device features:"); + features = new HashSet(); + if (pm != null) { + for (FeatureInfo fi : pm.getSystemAvailableFeatures()) { + features.add(fi.name); + logMsg.append('\n'); + logMsg.append(fi.name); + } + } + + cpuAbis = SupportedArchitectures.getAbis(); + + StringBuilder builder = new StringBuilder(); + boolean first = true; + for (String abi : cpuAbis) { + if (first) first = false; + else builder.append(", "); + builder.append(abi); + } + cpuAbisDesc = builder.toString(); + builder = null; + + Log.d("FDroid", logMsg.toString()); + } + + private boolean compatibleApi(DB.CommaSeparatedList nativecode) { + if (nativecode == null) return true; + for (String abi : nativecode) { + if (cpuAbis.contains(abi)) { + return true; + } + } + return false; + } + + public boolean isCompatible(Apk apk) { + if (!hasApi(apk.minSdkVersion)) { + apk.incompatible_reasons = DB.CommaSeparatedList.make(String.valueOf(apk.minSdkVersion)); + return false; + } + if (apk.features != null) { + for (String feat : apk.features) { + if (ignoreTouchscreen + && feat.equals("android.hardware.touchscreen")) { + // Don't check it! + } else if (!features.contains(feat)) { + apk.incompatible_reasons = DB.CommaSeparatedList.make(feat); + Log.d("FDroid", apk.id + " vercode " + apk.vercode + + " is incompatible based on lack of " + + feat); + return false; + } + } + } + if (!compatibleApi(apk.nativecode)) { + apk.incompatible_reasons = apk.nativecode; + Log.d("FDroid", apk.id + " vercode " + apk.vercode + + " only supports " + DB.CommaSeparatedList.str(apk.nativecode) + + " while your architectures are " + cpuAbisDesc); + return false; + } + return true; + } + } +} diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java new file mode 100644 index 000000000..b1fc75f30 --- /dev/null +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -0,0 +1,370 @@ +package org.fdroid.fdroid.data; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.net.Uri; +import android.provider.BaseColumns; +import android.util.Log; +import org.fdroid.fdroid.DB; + +import java.util.*; + +public class ApkProvider extends FDroidProvider { + + public static final class Helper { + + private Helper() {} + + public static void update(Context context, Apk apk, + String id, int versionCode) { + ContentResolver resolver = context.getContentResolver(); + Uri uri = getContentUri(id, versionCode); + resolver.update(uri, apk.toContentValues(), null, null); + } + + public static void update(Context context, Apk apk) { + ContentResolver resolver = context.getContentResolver(); + Uri uri = getContentUri(apk.id, apk.vercode); + resolver.update(uri, apk.toContentValues(), null, null); + } + + /** + * This doesn't do anything other than call "insert" on the content + * resolver, but I thought I'd put it here in the interests of having + * each of the CRUD methods available in the helper class. + */ + public static void insert(Context context, ContentValues values) { + ContentResolver resolver = context.getContentResolver(); + resolver.insert(getContentUri(), values); + } + + public static void insert(Context context, Apk apk) { + insert(context, apk.toContentValues()); + } + + public static List all(Context context) { + return all(context, DataColumns.ALL); + } + + public static List all(Context context, String[] projection) { + + ContentResolver resolver = context.getContentResolver(); + Uri uri = ApkProvider.getContentUri(); + Cursor cursor = resolver.query(uri, projection, null, null, null); + return cursorToList(cursor); + } + + private static List cursorToList(Cursor cursor) { + List apks = new ArrayList(); + if (cursor != null) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + apks.add(new Apk(cursor)); + cursor.moveToNext(); + } + cursor.close(); + } + return apks; + } + + public static void deleteApksByRepo(Context context, Repo repo) { + ContentResolver resolver = context.getContentResolver(); + Uri uri = getContentUri(); + String[] args = { Long.toString(repo.getId()) }; + String selection = DataColumns.REPO_ID + " = ?"; + resolver.delete(uri, selection + " = ?", args); + } + + public static void deleteApksByApp(Context context, DB.App app) { + ContentResolver resolver = context.getContentResolver(); + Uri uri = getContentUri(); + String[] args = { app.id }; + String selection = DataColumns.APK_ID + " = ?"; + resolver.delete(uri, selection, args); + } + + public static Apk find(Context context, String id, int versionCode) { + return find(context, id, versionCode, DataColumns.ALL); + } + + public static Apk find(Context context, String id, int versionCode, String[] projection) { + ContentResolver resolver = context.getContentResolver(); + Uri uri = getContentUri(id, versionCode); + Cursor cursor = resolver.query(uri, projection, null, null, null); + if (cursor != null && cursor.getCount() > 0) { + return new Apk(cursor); + } else { + return null; + } + } + + public static void delete(Context context, String id, int versionCode) { + ContentResolver resolver = context.getContentResolver(); + Uri uri = getContentUri(id, versionCode); + resolver.delete(uri, null, null); + } + } + + public interface DataColumns extends BaseColumns { + + public static String APK_ID = "id"; + public static String VERSION = "version"; + public static String REPO_ID = "repo"; + public static String HASH = "hash"; + public static String VERSION_CODE = "vercode"; + public static String NAME = "apkName"; + public static String SIZE = "size"; + public static String SIGNATURE = "sig"; + public static String SOURCE_NAME = "srcname"; + public static String MIN_SDK_VERSION = "minSdkVersion"; + public static String PERMISSIONS = "permissions"; + public static String FEATURES = "features"; + public static String NATIVE_CODE = "nativecode"; + public static String HASH_TYPE = "hashType"; + public static String ADDED_DATE = "added"; + public static String IS_COMPATIBLE = "compatible"; + public static String REPO_VERSION = "repoVersion"; + public static String REPO_ADDRESS = "repoAddress"; + + public static String[] ALL = { + _ID, APK_ID, VERSION, REPO_ID, HASH, VERSION_CODE, NAME, SIZE, + SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, PERMISSIONS, FEATURES, + NATIVE_CODE, HASH_TYPE, ADDED_DATE, IS_COMPATIBLE, + + REPO_VERSION, REPO_ADDRESS + }; + } + + private static final String PROVIDER_NAME = "ApkProvider"; + + private static final UriMatcher matcher = new UriMatcher(-1); + + public static Map REPO_FIELDS = new HashMap(); + + static { + REPO_FIELDS.put(DataColumns.REPO_VERSION, RepoProvider.DataColumns.VERSION); + REPO_FIELDS.put(DataColumns.REPO_ADDRESS, RepoProvider.DataColumns.ADDRESS); + + matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, null, CODE_LIST); + matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, "/*/#", CODE_SINGLE); + } + + public static Uri getContentUri() { + return Uri.parse("content://" + AUTHORITY + "." + PROVIDER_NAME); + } + + public static Uri getContentUri(String id, int versionCode) { + return getContentUri() + .buildUpon() + .appendPath(Integer.toString(versionCode)) + .appendPath(id) + .build(); + } + + @Override + protected String getTableName() { + return DBHelper.TABLE_APK; + } + + @Override + protected String getProviderName() { + return PROVIDER_NAME; + } + + protected UriMatcher getMatcher() { + return matcher; + } + + private static class QueryBuilder { + + private StringBuilder fields = new StringBuilder(); + private StringBuilder tables = new StringBuilder(DBHelper.TABLE_APK + " AS apk"); + private String selection = null; + private String orderBy = null; + + private boolean repoTableRequired = false; + + public void addField(String field) { + if (REPO_FIELDS.containsKey(field)) { + addRepoField(REPO_FIELDS.get(field), field); + } else if (field.equals(DataColumns._ID)) { + appendField("rowid", "apk", "_id"); + } else if (field.startsWith("COUNT")) { + appendField(field); + } else { + appendField(field, "apk"); + } + } + + public void addRepoField(String field, String alias) { + if (!repoTableRequired) { + repoTableRequired = true; + tables.append(" LEFT JOIN "); + tables.append(DBHelper.TABLE_REPO); + tables.append(" AS repo ON (apk.repo = repo._id) "); + } + appendField(field, "repo", alias); + } + + private void appendField(String field) { + appendField(field, null, null); + } + + private void appendField(String field, String tableAlias) { + appendField(field, tableAlias, null); + } + + private void appendField(String field, String tableAlias, + String fieldAlias) { + if (fields.length() != 0) { + fields.append(','); + } + + if (tableAlias != null) { + fields.append(tableAlias).append('.'); + } + + fields.append(field); + + if (fieldAlias != null) { + fields.append(" AS ").append(fieldAlias); + } + } + + public void addSelection(String selection) { + this.selection = selection; + } + + public void addOrderBy(String orderBy) { + this.orderBy = orderBy; + } + + public String toString() { + + StringBuilder suffix = new StringBuilder(); + if (selection != null) { + suffix.append(" WHERE ").append(selection); + } + + if (orderBy != null) { + suffix.append(" ORDER BY ").append(orderBy); + } + + return "SELECT " + fields + " FROM " + tables + suffix; + } + } + + private String appendPrimaryKeyToSelection(String selection) { + return (selection == null ? "" : selection + " AND ") + " id = ? and vercode = ?"; + } + + private String[] appendPrimaryKeyToArgs(Uri uri, String[] selectionArgs) { + List args = new ArrayList(selectionArgs.length + 2); + for (String arg : args) { + args.add(arg); + } + args.addAll(uri.getPathSegments()); + return (String[])args.toArray(); + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + + switch (matcher.match(uri)) { + case CODE_LIST: + break; + + case CODE_SINGLE: + selection = appendPrimaryKeyToSelection(selection); + selectionArgs = appendPrimaryKeyToArgs(uri, selectionArgs); + break; + + default: + Log.e("FDroid", "Invalid URI for apk content provider: " + uri); + throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri); + } + + QueryBuilder query = new QueryBuilder(); + for (String field : projection) { + query.addField(field); + } + query.addSelection(selection); + query.addOrderBy(sortOrder); + + Cursor cursor = read().rawQuery(query.toString(), selectionArgs); + cursor.setNotificationUri(getContext().getContentResolver(), uri); + return cursor; + } + + private static void removeRepoFields(ContentValues values) { + for (Map.Entry repoField : REPO_FIELDS.entrySet()) { + String field = repoField.getKey(); + if (values.containsKey(field)) { + Log.i("FDroid", "Cannot insert/update '" + field + "' field " + + "on apk table, as it belongs to the repo table. " + + "This field will be ignored."); + values.remove(field); + } + } + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + + removeRepoFields(values); + long id = write().insertOrThrow(getTableName(), null, values); + getContext().getContentResolver().notifyChange(uri, null); + return getContentUri( + values.getAsString(DataColumns.APK_ID), + values.getAsInteger(DataColumns.VERSION_CODE)); + + } + + @Override + public int delete(Uri uri, String where, String[] whereArgs) { + + switch (matcher.match(uri)) { + case CODE_LIST: + // Don't support deleting of multiple apks yet. + return 0; + + case CODE_SINGLE: + where = appendPrimaryKeyToSelection(where); + whereArgs = appendPrimaryKeyToArgs(uri, whereArgs); + break; + + default: + Log.e("FDroid", "Invalid URI for apk content provider: " + uri); + throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri); + } + + int rowsAffected = write().delete(getTableName(), where, whereArgs); + getContext().getContentResolver().notifyChange(uri, null); + return rowsAffected; + + } + + @Override + public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { + + switch (matcher.match(uri)) { + case CODE_LIST: + return 0; + + case CODE_SINGLE: + where = appendPrimaryKeyToSelection(where); + whereArgs = appendPrimaryKeyToArgs(uri, whereArgs); + break; + } + + removeRepoFields(values); + int numRows = write().update(getTableName(), values, where, whereArgs); + getContext().getContentResolver().notifyChange(uri, null); + return numRows; + + } + +} diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index 26c1b3582..58807514c 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -18,6 +18,11 @@ public class DBHelper extends SQLiteOpenHelper { public static final String TABLE_REPO = "fdroid_repo"; + // The TABLE_APK table stores details of all the application versions we + // know about. Each relates directly back to an entry in TABLE_APP. + // This information is retrieved from the repositories. + public static final String TABLE_APK = "fdroid_apk"; + private static final String CREATE_TABLE_REPO = "create table " + TABLE_REPO + " (_id integer primary key, " + "address text not null, " @@ -27,15 +32,26 @@ public class DBHelper extends SQLiteOpenHelper { + "version integer not null default 0, " + "lastetag text, lastUpdated string);"; - private static final String CREATE_TABLE_APK = "create table " + DB.TABLE_APK - + " ( " + "id text not null, " + "version text not null, " - + "repo integer not null, " + "hash text not null, " - + "vercode int not null," + "apkName text not null, " - + "size int not null," + "sig string," + "srcname string," - + "minSdkVersion integer," + "permissions string," - + "features string," + "nativecode string," - + "hashType string," + "added string," - + "compatible int not null," + "primary key(id,vercode));"; + private static final String CREATE_TABLE_APK = + "CREATE TABLE " + TABLE_APK + " ( " + + "id text not null, " + + "version text not null, " + + "repo integer not null, " + + "hash text not null, " + + "vercode int not null," + + "apkName text not null, " + + "size int not null, " + + "sig string, " + + "srcname string, " + + "minSdkVersion integer, " + + "permissions string, " + + "features string, " + + "nativecode string, " + + "hashType string, " + + "added string, " + + "compatible int not null, " + + "primary key(id, vercode)" + + ");"; private static final String CREATE_TABLE_APP = "create table " + DB.TABLE_APP + " ( " + "id text not null, " + "name text not null, " @@ -308,7 +324,7 @@ public class DBHelper extends SQLiteOpenHelper { context.getSharedPreferences("FDroid", Context.MODE_PRIVATE).edit() .putBoolean("triedEmptyUpdate", false).commit(); db.execSQL("drop table " + DB.TABLE_APP); - db.execSQL("drop table " + DB.TABLE_APK); + db.execSQL("drop table " + TABLE_APK); db.execSQL("update " + TABLE_REPO + " set lastetag = NULL"); createAppApk(db); } @@ -317,8 +333,8 @@ public class DBHelper extends SQLiteOpenHelper { db.execSQL(CREATE_TABLE_APP); db.execSQL("create index app_id on " + DB.TABLE_APP + " (id);"); db.execSQL(CREATE_TABLE_APK); - db.execSQL("create index apk_vercode on " + DB.TABLE_APK + " (vercode);"); - db.execSQL("create index apk_id on " + DB.TABLE_APK + " (id);"); + db.execSQL("create index apk_vercode on " + TABLE_APK + " (vercode);"); + db.execSQL("create index apk_id on " + TABLE_APK + " (id);"); } private static boolean columnExists(SQLiteDatabase db, diff --git a/src/org/fdroid/fdroid/data/Repo.java b/src/org/fdroid/fdroid/data/Repo.java index ca01bde09..051c634ff 100644 --- a/src/org/fdroid/fdroid/data/Repo.java +++ b/src/org/fdroid/fdroid/data/Repo.java @@ -10,7 +10,7 @@ import java.net.URL; import java.text.ParseException; import java.util.Date; -public class Repo { +public class Repo extends ValueObject{ private long id; @@ -46,14 +46,7 @@ public class Repo { } else if (column.equals(RepoProvider.DataColumns.IN_USE)) { inuse = cursor.getInt(i) == 1; } else if (column.equals(RepoProvider.DataColumns.LAST_UPDATED)) { - String dateString = cursor.getString(i); - if (dateString != null) { - try { - lastUpdated = DB.DATE_FORMAT.parse(dateString); - } catch (ParseException e) { - Log.e("FDroid", "Error parsing date " + dateString); - } - } + lastUpdated = toDate(cursor.getString(i)); } else if (column.equals(RepoProvider.DataColumns.MAX_AGE)) { maxage = cursor.getInt(i); } else if (column.equals(RepoProvider.DataColumns.VERSION)) { @@ -78,13 +71,6 @@ public class Repo { return address; } - public int getNumberOfApps() { - DB db = DB.getDB(); - int count = db.countAppsForRepo(id); - DB.releaseDB(); - return count; - } - public boolean isSigned() { return this.pubkey != null && this.pubkey.length() > 0; } diff --git a/src/org/fdroid/fdroid/data/RepoProvider.java b/src/org/fdroid/fdroid/data/RepoProvider.java index 3f955fd37..6466af81f 100644 --- a/src/org/fdroid/fdroid/data/RepoProvider.java +++ b/src/org/fdroid/fdroid/data/RepoProvider.java @@ -76,6 +76,7 @@ public class RepoProvider extends FDroidProvider { repos.add(new Repo(cursor)); cursor.moveToNext(); } + cursor.close(); } return repos; } @@ -163,6 +164,20 @@ public class RepoProvider extends FDroidProvider { } } + public static int countAppsForRepo(ContentResolver resolver, + long repoId) { + String[] projection = { "COUNT(distinct id)" }; + String selection = "repo = ?"; + String[] args = { Long.toString(repoId) }; + Uri apkUri = ApkProvider.getContentUri(); + Cursor result = resolver.query(apkUri, projection, selection, args, null); + if (result != null && result.getCount() > 0) { + result.moveToFirst(); + return result.getInt(0); + } else { + return 0; + } + } } public interface DataColumns extends BaseColumns { @@ -189,12 +204,12 @@ public class RepoProvider extends FDroidProvider { private static final UriMatcher matcher = new UriMatcher(-1); static { - matcher.addURI(AUTHORITY, PROVIDER_NAME, CODE_LIST); - matcher.addURI(AUTHORITY, PROVIDER_NAME + "/#", CODE_SINGLE); + matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, null, CODE_LIST); + matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, "#", CODE_SINGLE); } public static Uri getContentUri() { - return Uri.parse("content://" + AUTHORITY + "/" + PROVIDER_NAME); + return Uri.parse("content://" + AUTHORITY + "." + PROVIDER_NAME); } public static Uri getContentUri(long repoId) { @@ -226,8 +241,8 @@ public class RepoProvider extends FDroidProvider { break; case CODE_SINGLE: - selection = ( selection == null ? "" : selection ) + - "_ID = " + uri.getLastPathSegment(); + selection = ( selection == null ? "" : selection + " AND " ) + + DataColumns._ID + " = " + uri.getLastPathSegment(); break; default: @@ -287,7 +302,7 @@ public class RepoProvider extends FDroidProvider { return 0; case CODE_SINGLE: - where = ( where == null ? "" : where ) + + where = ( where == null ? "" : where + " AND " ) + "_ID = " + uri.getLastPathSegment(); break; diff --git a/src/org/fdroid/fdroid/data/ValueObject.java b/src/org/fdroid/fdroid/data/ValueObject.java new file mode 100644 index 000000000..fb936cc77 --- /dev/null +++ b/src/org/fdroid/fdroid/data/ValueObject.java @@ -0,0 +1,23 @@ +package org.fdroid.fdroid.data; + +import android.util.Log; +import org.fdroid.fdroid.DB; + +import java.text.ParseException; +import java.util.Date; + +abstract class ValueObject { + + static Date toDate(String string) { + Date date = null; + if (string != null) { + try { + date = DB.DATE_FORMAT.parse(string); + } catch (ParseException e) { + Log.e("FDroid", "Error parsing date " + string); + } + } + return date; + } + +} diff --git a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java index 1e23b034e..d86807c0d 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java @@ -148,7 +148,10 @@ public class RepoDetailsFragment extends Fragment { TextView lastUpdated = (TextView)repoView.findViewById(R.id.text_last_update); name.setText(repo.getName()); - numApps.setText(Integer.toString(repo.getNumberOfApps())); + + int appCount = RepoProvider.Helper.countAppsForRepo( + getActivity().getContentResolver(), repo.getId()); + numApps.setText(Integer.toString(appCount)); setupDescription(repoView, repo); setupRepoFingerprint(repoView, repo); From da8e41249bbfc6ec57959857493b5ea438c77024 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Sun, 2 Feb 2014 19:38:36 +1100 Subject: [PATCH 062/282] Removed DB, implemented AppProvider. Yay! As expected, a lot of the stuff in DB class is due to UpdateService requiring it to process the downloaded indexes and insert data into the database. Thus, this change is about removing that stuff from the DB class and migrating to ContentProviders. This required a bit of a change to the way that UpdateService decides what to do with the data from indexes, but I hope it will make understanding and changing UpdateService easier in the long term. For example, it used to read the app details from database, then if a repo wasn't updated (due to unchanged index) then it would take the app details for that repo from the list of apps, and re-update the database (or something like that). Now, it has been refactored into the following methods: * updateOrInsertApps(appsToUpdate); * updateOrInsertApks(apksToUpdate); * removeApksFromRepos(disabledRepos); * removeApksNoLongerInRepo(appsToUpdate, updatedRepos); * removeAppsWithoutApks(); * and probably some others... Which hopefully are self-explanitory. The recent change to implement single repo updates was re-implemented with in light of the methods above. The interface to UpdateService for scheduling a single repo update is the same as it was before, but the implementation is completely different. Still works though. Using batch content provider operations for repo updates, but they suffer from the problem of not all being under the same transaction, so if an insert/update stuffs up half way through, we are left with only half of the update being complete. In the future, if there is some way to implement notifications from the content provider's applyBatch method, then we can do it all in the one transaction, and still have notifications. Currently we break it into several calls to applyBatch (and hence several transactions) to inform the user of the progress. Also adding the beginnings of some tests for AppProvider. In the future, I'll work on adding better coverage, including instrumentation to test UI features. ========================== Below is a list of many of the minor changes that also happened along the way ========================== Make "Can update" tab stay up to date using content observer, rather than manually deciding when to refresh the tab label as before. The installed app list is now cached in Utils, because it is invoked quite a few times, especially when rendering the app lists. The cache is invalidated when PackageReceiver is notified of new apps. The content providers don't notify changes if we are in batch mode. I've left the notification at the end of the batch updates as the responsibility of the UpdateService. However, it would be nice if this was somehow handled by the content, as they are really the ones who should worry about it. Made curVersion, curVercode and curApk work with providers. This was done by removing curApk (otherwise we'd need to query the db each time we fetched one app to get a reference to that apk (resulting in hundreds of queries). Instead, UpdateService now calculates curVercode and curVersion and saves them to the database. We then use these where possible. If we really need curApk (because we want info other than its version and code) we still have the option of ApkProvider.Helper.find(app.id, app.curVercode). I considered putting this inside the app value object, e.g. in getCurApk() but thought better of it as it will likely result in people invoking it all the time, without realising it causes a DB query. incompatibleReasons required a minor UI tweak, removing the "min sdk" ui element from the Apk list. It is replaced by the "Requires: %s" view (which only appears when the app is incompatible). In the process, and in response to some feedback from mvdan, I left the min sdk in there, but only made it show when in "expert mode", just like the architecture. In order to make the "installed apps" query work under test conditions, needed to change the way the InstalledApkCache be replaceable with a mock object. Pause UIL loading on fast scroll of list, as the list was very choppy for some reason. Re-added "Last repo scan" info to the Manage Repo list view. Fixed up some misc TODO's, removed some unused/empty functions. --- AndroidManifest.xml | 5 + TODO | 3 - res/layout/apklistitem.xml | 7 + res/values/array.xml | 6 - res/values/strings.xml | 6 +- res/xml/preferences.xml | 5 - src/org/fdroid/fdroid/AppDetails.java | 254 +++-- src/org/fdroid/fdroid/AppFilter.java | 23 +- src/org/fdroid/fdroid/AppListManager.java | 248 ----- .../fdroid/fdroid/CompatibilityChecker.java | 103 ++ src/org/fdroid/fdroid/DB.java | 943 ------------------ src/org/fdroid/fdroid/Downloader.java | 12 +- src/org/fdroid/fdroid/FDroid.java | 60 +- src/org/fdroid/fdroid/FDroidApp.java | 80 +- src/org/fdroid/fdroid/ManageRepo.java | 73 +- src/org/fdroid/fdroid/PackageReceiver.java | 1 + src/org/fdroid/fdroid/Preferences.java | 76 +- .../fdroid/fdroid/PreferencesActivity.java | 10 +- src/org/fdroid/fdroid/RepoXMLHandler.java | 102 +- src/org/fdroid/fdroid/SearchResults.java | 57 +- src/org/fdroid/fdroid/UpdateService.java | 583 +++++++---- src/org/fdroid/fdroid/Utils.java | 183 +++- src/org/fdroid/fdroid/data/Apk.java | 162 +-- src/org/fdroid/fdroid/data/ApkProvider.java | 196 +++- src/org/fdroid/fdroid/data/App.java | 230 +++++ src/org/fdroid/fdroid/data/AppProvider.java | 481 +++++++++ src/org/fdroid/fdroid/data/DBHelper.java | 61 +- .../fdroid/fdroid/data/FDroidProvider.java | 46 +- .../fdroid/fdroid/data/QuerySelection.java | 79 ++ src/org/fdroid/fdroid/data/Repo.java | 11 +- src/org/fdroid/fdroid/data/RepoProvider.java | 24 +- src/org/fdroid/fdroid/data/ValueObject.java | 13 +- .../fdroid/fdroid/updater/RepoUpdater.java | 18 +- .../fdroid/updater/SignedRepoUpdater.java | 3 +- .../fdroid/updater/UnsignedRepoUpdater.java | 2 - .../fdroid/fdroid/views/AppListAdapter.java | 170 ++-- .../views/AppListFragmentPageAdapter.java | 21 +- .../fdroid/views/AvailableAppListAdapter.java | 14 +- .../fdroid/views/CanUpdateAppListAdapter.java | 14 +- .../fdroid/views/InstalledAppListAdapter.java | 14 +- .../fdroid/views/RepoDetailsActivity.java | 4 + .../views/fragments/AppListFragment.java | 150 ++- .../fragments/AvailableAppsFragment.java | 94 +- .../fragments/CanUpdateAppsFragment.java | 25 +- .../fragments/InstalledAppsFragment.java | 25 +- test/.gitignore | 10 + test/AndroidManifest.xml | 21 + test/ant.properties | 18 + test/build.xml | 92 ++ test/proguard-project.txt | 20 + {tests => test}/project.properties | 2 +- .../test/ProviderTestCase2MockContext.java | 228 +++++ test/src/mock/MockContextEmptyComponents.java | 14 + .../mock/MockContextSwappableComponents.java | 32 + test/src/mock/MockEmptyPackageManager.java | 16 + test/src/mock/MockEmptyResources.java | 12 + .../mock/MockInstallablePackageManager.java | 26 + .../org/fdroid/fdroid/AppProviderTest.java | 138 +++ .../org/fdroid/fdroid/FDroidProviderTest.java | 73 ++ test/src/org/fdroid/fdroid/FDroidTest.java | 14 + .../fdroid/mock/MockInstalledApkCache.java | 16 + .../org/fdroid/fdroid/tests/BuildConfig.java | 8 - .../gen/org/fdroid/fdroid/tests/Manifest.java | 7 - tests/gen/org/fdroid/fdroid/tests/R.java | 7 - tests/local.properties | 10 - 65 files changed, 3264 insertions(+), 2197 deletions(-) delete mode 100644 TODO delete mode 100644 src/org/fdroid/fdroid/AppListManager.java create mode 100644 src/org/fdroid/fdroid/CompatibilityChecker.java delete mode 100644 src/org/fdroid/fdroid/DB.java create mode 100644 src/org/fdroid/fdroid/data/App.java create mode 100644 src/org/fdroid/fdroid/data/AppProvider.java create mode 100644 src/org/fdroid/fdroid/data/QuerySelection.java create mode 100644 test/.gitignore create mode 100644 test/AndroidManifest.xml create mode 100644 test/ant.properties create mode 100644 test/build.xml create mode 100644 test/proguard-project.txt rename {tests => test}/project.properties (96%) create mode 100644 test/src/android/test/ProviderTestCase2MockContext.java create mode 100644 test/src/mock/MockContextEmptyComponents.java create mode 100644 test/src/mock/MockContextSwappableComponents.java create mode 100644 test/src/mock/MockEmptyPackageManager.java create mode 100644 test/src/mock/MockEmptyResources.java create mode 100644 test/src/mock/MockInstallablePackageManager.java create mode 100644 test/src/org/fdroid/fdroid/AppProviderTest.java create mode 100644 test/src/org/fdroid/fdroid/FDroidProviderTest.java create mode 100644 test/src/org/fdroid/fdroid/FDroidTest.java create mode 100644 test/src/org/fdroid/fdroid/mock/MockInstalledApkCache.java delete mode 100644 tests/gen/org/fdroid/fdroid/tests/BuildConfig.java delete mode 100644 tests/gen/org/fdroid/fdroid/tests/Manifest.java delete mode 100644 tests/gen/org/fdroid/fdroid/tests/R.java delete mode 100644 tests/local.properties diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 248240f6c..56149a5c2 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -35,6 +35,11 @@ android:theme="@style/AppThemeDark" android:supportsRtl="false" > + + + + diff --git a/res/values/array.xml b/res/values/array.xml index 7481d8fa6..bea7d606e 100644 --- a/res/values/array.xml +++ b/res/values/array.xml @@ -12,10 +12,4 @@ Dark Light - - - Off (unsafe) - Normal - Full - diff --git a/res/values/strings.xml b/res/values/strings.xml index 558a506b7..3ded618fa 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -127,8 +127,6 @@ Search applications - Database sync mode - Application compatibility Incompatible versions Show app versions incompatible with the device @@ -155,6 +153,7 @@ Processing application\n%2$d of %3$d from\n%1$s Connecting to\n%1$s Checking apps compatibility with your device… + Saving application details (%1$d%%) No permissions are used. Permissions for version %s Show permissions @@ -195,7 +194,8 @@ Disabled "%1$s".\n\nYou will need to re-enable this repository to install apps from it. - %s or later + Android %s or later Your device is not on the same WiFi as the local repo you just added! Try joining this network: %s + Requires: %1$s diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 7b8cbd11e..5ae1f7d10 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -49,10 +49,5 @@ - diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index d39486d8a..7e2a09053 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -21,13 +21,12 @@ package org.fdroid.fdroid; import java.io.File; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import org.fdroid.fdroid.data.Apk; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; +import android.content.*; +import android.widget.*; +import org.fdroid.fdroid.data.*; import org.xml.sax.XMLReader; import android.app.AlertDialog; @@ -38,19 +37,10 @@ import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.preference.PreferenceManager; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; import android.content.pm.PackageManager; import android.content.pm.PackageInfo; import android.content.pm.Signature; import android.content.pm.PackageManager.NameNotFoundException; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; import android.text.Editable; import android.text.Html; import android.text.Html.TagHandler; @@ -64,7 +54,6 @@ import android.view.MenuItem; import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; -import android.widget.BaseAdapter; import android.graphics.Bitmap; import android.support.v4.app.NavUtils; @@ -73,7 +62,7 @@ import android.support.v4.view.MenuItemCompat; import org.fdroid.fdroid.compat.PackageManagerCompat; import org.fdroid.fdroid.compat.ActionBarCompat; import org.fdroid.fdroid.compat.MenuManager; -import org.fdroid.fdroid.DB.CommaSeparatedList; +import org.fdroid.fdroid.Utils.CommaSeparatedList; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; @@ -83,63 +72,40 @@ public class AppDetails extends ListActivity { private static final int REQUEST_INSTALL = 0; private static final int REQUEST_UNINSTALL = 1; + private ApkListAdapter adapter; private static class ViewHolder { TextView version; TextView status; TextView size; TextView api; + TextView incompatibleReasons; TextView buildtype; TextView added; TextView nativecode; } - private class ApkListAdapter extends BaseAdapter { + private class ApkListAdapter extends ArrayAdapter { - private List items; - private LayoutInflater mInflater; + private LayoutInflater mInflater = (LayoutInflater) mctx.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); - public ApkListAdapter(Context context, List items) { - this.items = new ArrayList(); - if (items != null) { - for (Apk apk : items) { - this.addItem(apk); + public ApkListAdapter(Context context, App app) { + super(context, 0); + List apks = ApkProvider.Helper.findByApp(context.getContentResolver(), app.id); + for (Apk apk : apks ) { + if (apk.compatible || pref_incompatibleVersions) { + add(apk); } } - mInflater = (LayoutInflater) mctx.getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - } - public void addItem(Apk apk) { - if (apk.compatible || pref_incompatibleVersions) { - items.add(apk); - } - } - - public List getItems() { - return items; - } - - @Override - public int getCount() { - return items.size(); - } - - @Override - public Object getItem(int position) { - return items.get(position); - } - - @Override - public long getItemId(int position) { - return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { java.text.DateFormat df = DateFormat.getDateFormat(mctx); - Apk apk = items.get(position); + Apk apk = getItem(position); ViewHolder holder; if (convertView == null) { @@ -150,6 +116,7 @@ public class AppDetails extends ListActivity { holder.status = (TextView) convertView.findViewById(R.id.status); holder.size = (TextView) convertView.findViewById(R.id.size); holder.api = (TextView) convertView.findViewById(R.id.api); + holder.incompatibleReasons = (TextView) convertView.findViewById(R.id.incompatible_reasons); holder.buildtype = (TextView) convertView.findViewById(R.id.buildtype); holder.added = (TextView) convertView.findViewById(R.id.added); holder.nativecode = (TextView) convertView.findViewById(R.id.nativecode); @@ -161,9 +128,9 @@ public class AppDetails extends ListActivity { holder.version.setText(getString(R.string.version) + " " + apk.version - + (apk == app.curApk ? " ☆" : "")); + + (apk.vercode == app.curVercode ? " ☆" : "")); - if (apk.vercode == app.installedVerCode + if (apk.vercode == app.getInstalledVerCode(getContext()) && mInstalledSigID != null && apk.sig != null && apk.sig.equals(mInstalledSigID)) { holder.status.setText(getString(R.string.inst)); @@ -171,14 +138,14 @@ public class AppDetails extends ListActivity { holder.status.setText(getString(R.string.not_inst)); } - if (apk.detail_size > 0) { - holder.size.setText(Utils.getFriendlySize(apk.detail_size)); + if (apk.size > 0) { + holder.size.setText(Utils.getFriendlySize(apk.size)); holder.size.setVisibility(View.VISIBLE); } else { holder.size.setVisibility(View.GONE); } - if (apk.minSdkVersion > 0) { + if (pref_expert && apk.minSdkVersion > 0) { holder.api.setText(getString(R.string.minsdk_or_later, Utils.getAndroidVersionName(apk.minSdkVersion))); holder.api.setVisibility(View.VISIBLE); @@ -208,8 +175,13 @@ public class AppDetails extends ListActivity { } if (apk.incompatible_reasons != null) { - holder.api.setText(apk.incompatible_reasons.toString()); - holder.api.setVisibility(View.VISIBLE); + holder.incompatibleReasons.setText( + getResources().getString( + R.string.requires_features, + apk.incompatible_reasons.toPrettyString())); + holder.incompatibleReasons.setVisibility(View.VISIBLE); + } else { + holder.incompatibleReasons.setVisibility(View.GONE); } // Disable it all if it isn't compatible... @@ -219,13 +191,14 @@ public class AppDetails extends ListActivity { holder.status, holder.size, holder.api, + holder.incompatibleReasons, holder.buildtype, holder.added, holder.nativecode }; - for (View view : views) { - view.setEnabled(apk.compatible); + for (View v : views) { + v.setEnabled(apk.compatible); } return convertView; @@ -248,7 +221,7 @@ public class AppDetails extends ListActivity { private static final int FLATTR = Menu.FIRST + 13; private static final int DONATE_URL = Menu.FIRST + 14; - private DB.App app; + private App app; private String appid; private PackageManager mPm; private DownloadHandler downloadHandler; @@ -336,8 +309,8 @@ public class AppDetails extends ListActivity { headerView = new LinearLayout(this); ListView lv = (ListView) findViewById(android.R.id.list); lv.addHeaderView(headerView); - ApkListAdapter la = new ApkListAdapter(this, app.apks); - setListAdapter(la); + adapter = new ApkListAdapter(this, app); + setListAdapter(adapter); startViews(); @@ -378,17 +351,24 @@ public class AppDetails extends ListActivity { } if (app != null && (app.ignoreAllUpdates != startingIgnoreAll || app.ignoreThisUpdate != startingIgnoreThis)) { - try { - DB db = DB.getDB(); - db.setIgnoreUpdates(app.id, - app.ignoreAllUpdates, app.ignoreThisUpdate); - } finally { - DB.releaseDB(); - } + setIgnoreUpdates(app.id, app.ignoreAllUpdates, app.ignoreThisUpdate); } super.onPause(); } + public void setIgnoreUpdates(String appId, boolean ignoreAll, int ignoreVersionCode) { + + Uri uri = AppProvider.getContentUri(appId); + + ContentValues values = new ContentValues(2); + values.put(AppProvider.DataColumns.IGNORE_ALLUPDATES, ignoreAll ? 1 : 0); + values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreVersionCode); + + getContentResolver().update(uri, values, null, null); + + } + + @Override public Object onRetainNonConfigurationInstance() { stateRetained = true; @@ -423,15 +403,11 @@ public class AppDetails extends ListActivity { Log.d("FDroid", "Getting application details for " + appid); app = null; + if (appid != null && appid.length() > 0) { - List apps = ((FDroidApp) getApplication()).getApps(); - for (DB.App tapp : apps) { - if (tapp.id.equals(appid)) { - app = tapp; - break; - } - } + app = AppProvider.Helper.findById(getContentResolver(), appid); } + if (app == null) { Toast toast = Toast.makeText(this, getString(R.string.no_such_app), Toast.LENGTH_LONG); @@ -440,23 +416,13 @@ public class AppDetails extends ListActivity { return false; } - // Make sure the app is populated. - try { - DB db = DB.getDB(); - db.populateDetails(app, 0); - } catch (Exception ex) { - Log.d("FDroid", "Failed to populate app - " + ex.getMessage()); - } finally { - DB.releaseDB(); - } - startingIgnoreAll = app.ignoreAllUpdates; startingIgnoreThis = app.ignoreThisUpdate; // Get the signature of the installed package... mInstalledSignature = null; mInstalledSigID = null; - if (app.installedVersion != null) { + if (app.getInstalledVersion(this) != null) { PackageManager pm = getBaseContext().getPackageManager(); try { PackageInfo pi = pm.getPackageInfo(appid, @@ -545,7 +511,7 @@ public class AppDetails extends ListActivity { } } Spanned desc = Html.fromHtml( - app.detail_description, null, new HtmlTagHandler()); + app.description, null, new HtmlTagHandler()); tv.setText(desc.subSequence(0, desc.length() - 2)); tv = (TextView) infoView.findViewById(R.id.appid); @@ -557,11 +523,20 @@ public class AppDetails extends ListActivity { tv = (TextView) infoView.findViewById(R.id.summary); tv.setText(app.summary); - if (pref_permissions && app.curApk != null && - (app.curApk.compatible || pref_incompatibleVersions)) { + Apk curApk = null; + for (int i = 0; i < adapter.getCount(); i ++) { + Apk apk = adapter.getItem(i); + if (apk.vercode == app.curVercode) { + curApk = apk; + break; + } + } + + if (pref_permissions && !adapter.isEmpty() && + ((curApk != null && curApk.compatible) || pref_incompatibleVersions)) { tv = (TextView) infoView.findViewById(R.id.permissions_list); - CommaSeparatedList permsList = app.curApk.detail_permissions; + CommaSeparatedList permsList = adapter.getItem(0).permissions; if (permsList == null) { tv.setText(getString(R.string.no_permissions)); } else { @@ -586,7 +561,7 @@ public class AppDetails extends ListActivity { } tv = (TextView) infoView.findViewById(R.id.permissions); tv.setText(getString( - R.string.permissions_for_long, app.apks.get(0).version)); + R.string.permissions_for_long, adapter.getItem(0).version)); } else { infoView.findViewById(R.id.permissions).setVisibility(View.GONE); infoView.findViewById(R.id.permissions_list).setVisibility(View.GONE); @@ -631,15 +606,14 @@ public class AppDetails extends ListActivity { private void updateViews() { // Refresh the list... - ApkListAdapter la = (ApkListAdapter) getListAdapter(); - la.notifyDataSetChanged(); + adapter.notifyDataSetChanged(); TextView tv = (TextView) findViewById(R.id.status); - if (app.installedVersion == null) + if (app.getInstalledVersion(this) == null) tv.setText(getString(R.string.details_notinstalled)); else tv.setText(getString(R.string.details_installed, - app.installedVersion)); + app.getInstalledVersion(this))); tv = (TextView) infoView.findViewById(R.id.signature); if (pref_expert && mInstalledSignature != null) { @@ -653,10 +627,10 @@ public class AppDetails extends ListActivity { @Override protected void onListItemClick(ListView l, View v, int position, long id) { - app.curApk = app.apks.get(position - l.getHeaderViewsCount()); - if (app.installedVerCode == app.curApk.vercode) + final Apk apk = adapter.getItem(position - l.getHeaderViewsCount()); + if (app.getInstalledVerCode(this) == apk.vercode) removeApk(app.id); - else if (app.installedVerCode > app.curApk.vercode) { + else if (app.getInstalledVerCode(this) > apk.vercode) { AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this); ask_alrt.setMessage(getString(R.string.installDowngrade)); ask_alrt.setPositiveButton(getString(R.string.yes), @@ -664,7 +638,7 @@ public class AppDetails extends ListActivity { @Override public void onClick(DialogInterface dialog, int whichButton) { - install(); + install(apk); } }); ask_alrt.setNegativeButton(getString(R.string.no), @@ -677,7 +651,7 @@ public class AppDetails extends ListActivity { AlertDialog alert = ask_alrt.create(); alert.show(); } else - install(); + install(apk); } @Override @@ -687,20 +661,23 @@ public class AppDetails extends ListActivity { menu.clear(); if (app == null) return true; - if (app.toUpdate) { + if (app.canAndWantToUpdate(this)) { MenuItemCompat.setShowAsAction(menu.add( Menu.NONE, INSTALL, 0, R.string.menu_upgrade) .setIcon(R.drawable.ic_menu_refresh), MenuItemCompat.SHOW_AS_ACTION_ALWAYS | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT); } - if (app.installedVersion == null && app.curApk != null) { + + // Check count > 0 due to incompatible apps resulting in an empty list. + if (app.getInstalledVersion(this) == null && app.curVercode > 0 && + adapter.getCount() > 0) { MenuItemCompat.setShowAsAction(menu.add( Menu.NONE, INSTALL, 1, R.string.menu_install) .setIcon(android.R.drawable.ic_menu_add), MenuItemCompat.SHOW_AS_ACTION_ALWAYS | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT); - } else if (app.installedVersion != null) { + } else if (app.getInstalledVersion(this) != null) { MenuItemCompat.setShowAsAction(menu.add( Menu.NONE, UNINSTALL, 1, R.string.menu_uninstall) .setIcon(android.R.drawable.ic_menu_delete), @@ -727,40 +704,40 @@ public class AppDetails extends ListActivity { .setCheckable(true) .setChecked(app.ignoreAllUpdates); - if (app.hasUpdates) { + if (app.hasUpdates(this)) { menu.add(Menu.NONE, IGNORETHIS, 2, R.string.menu_ignore_this) .setIcon(android.R.drawable.ic_menu_close_clear_cancel) .setCheckable(true) - .setChecked(app.ignoreThisUpdate >= app.curApk.vercode); + .setChecked(app.ignoreThisUpdate >= app.curVercode); } - if (app.detail_webURL.length() > 0) { + if (app.webURL.length() > 0) { menu.add(Menu.NONE, WEBSITE, 3, R.string.menu_website).setIcon( android.R.drawable.ic_menu_view); } - if (app.detail_trackerURL.length() > 0) { + if (app.trackerURL.length() > 0) { menu.add(Menu.NONE, ISSUES, 4, R.string.menu_issues).setIcon( android.R.drawable.ic_menu_view); } - if (app.detail_sourceURL.length() > 0) { + if (app.sourceURL.length() > 0) { menu.add(Menu.NONE, SOURCE, 5, R.string.menu_source).setIcon( android.R.drawable.ic_menu_view); } - if (app.detail_bitcoinAddr != null || app.detail_litecoinAddr != null || - app.detail_dogecoinAddr != null || - app.detail_flattrID != null || app.detail_donateURL != null) { + if (app.bitcoinAddr != null || app.litecoinAddr != null || + app.dogecoinAddr != null || + app.flattrID != null || app.donateURL != null) { SubMenu donate = menu.addSubMenu(Menu.NONE, DONATE, 7, R.string.menu_donate).setIcon( android.R.drawable.ic_menu_send); - if (app.detail_bitcoinAddr != null) + if (app.bitcoinAddr != null) donate.add(Menu.NONE, BITCOIN, 8, R.string.menu_bitcoin); - if (app.detail_litecoinAddr != null) + if (app.litecoinAddr != null) donate.add(Menu.NONE, LITECOIN, 8, R.string.menu_litecoin); - if (app.detail_dogecoinAddr != null) + if (app.dogecoinAddr != null) donate.add(Menu.NONE, DOGECOIN, 8, R.string.menu_dogecoin); - if (app.detail_flattrID != null) + if (app.flattrID != null) donate.add(Menu.NONE, FLATTR, 9, R.string.menu_flattr); - if (app.detail_donateURL != null) + if (app.donateURL != null) donate.add(Menu.NONE, DONATE_URL, 10, R.string.menu_website); } @@ -798,8 +775,10 @@ public class AppDetails extends ListActivity { case INSTALL: // Note that this handles updating as well as installing. - if (app.curApk != null) - install(); + if (app.curVercode > 0) { + final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.curVercode); + install(apkToInstall); + } return true; case UNINSTALL: @@ -812,43 +791,43 @@ public class AppDetails extends ListActivity { return true; case IGNORETHIS: - if (app.ignoreThisUpdate >= app.curApk.vercode) + if (app.ignoreThisUpdate >= app.curVercode) app.ignoreThisUpdate = 0; else - app.ignoreThisUpdate = app.curApk.vercode; + app.ignoreThisUpdate = app.curVercode; item.setChecked(app.ignoreThisUpdate > 0); return true; case WEBSITE: - tryOpenUri(app.detail_webURL); + tryOpenUri(app.webURL); return true; case ISSUES: - tryOpenUri(app.detail_trackerURL); + tryOpenUri(app.trackerURL); return true; case SOURCE: - tryOpenUri(app.detail_sourceURL); + tryOpenUri(app.sourceURL); return true; case BITCOIN: - tryOpenUri("bitcoin:" + app.detail_bitcoinAddr); + tryOpenUri("bitcoin:" + app.bitcoinAddr); return true; case LITECOIN: - tryOpenUri("litecoin:" + app.detail_litecoinAddr); + tryOpenUri("litecoin:" + app.litecoinAddr); return true; case DOGECOIN: - tryOpenUri("dogecoin:" + app.detail_dogecoinAddr); + tryOpenUri("dogecoin:" + app.dogecoinAddr); return true; case FLATTR: - tryOpenUri("https://flattr.com/thing/" + app.detail_flattrID); + tryOpenUri("https://flattr.com/thing/" + app.flattrID); return true; case DONATE_URL: - tryOpenUri(app.detail_donateURL); + tryOpenUri(app.donateURL); return true; } @@ -856,17 +835,16 @@ public class AppDetails extends ListActivity { } // Install the version of this app denoted by 'app.curApk'. - private void install() { - + private void install(final Apk apk) { String [] projection = { RepoProvider.DataColumns.ADDRESS }; Repo repo = RepoProvider.Helper.findById( - getContentResolver(), app.curApk.repo, projection); + getContentResolver(), apk.repo, projection); if (repo == null || repo.address == null) { return; } final String repoaddress = repo.address; - if (!app.curApk.compatible) { + if (!apk.compatible) { AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this); ask_alrt.setMessage(getString(R.string.installIncompatible)); ask_alrt.setPositiveButton(getString(R.string.yes), @@ -874,7 +852,7 @@ public class AppDetails extends ListActivity { @Override public void onClick(DialogInterface dialog, int whichButton) { - downloadHandler = new DownloadHandler(app.curApk, + downloadHandler = new DownloadHandler(apk, repoaddress, Utils .getApkCacheDir(getBaseContext())); } @@ -890,8 +868,8 @@ public class AppDetails extends ListActivity { alert.show(); return; } - if (mInstalledSigID != null && app.curApk.sig != null - && !app.curApk.sig.equals(mInstalledSigID)) { + if (mInstalledSigID != null && apk.sig != null + && !apk.sig.equals(mInstalledSigID)) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(R.string.SignatureMismatch).setPositiveButton( getString(R.string.ok), @@ -905,7 +883,7 @@ public class AppDetails extends ListActivity { alert.show(); return; } - downloadHandler = new DownloadHandler(app.curApk, repoaddress, + downloadHandler = new DownloadHandler(apk, repoaddress, Utils.getApkCacheDir(getBaseContext())); } @@ -937,7 +915,7 @@ public class AppDetails extends ListActivity { startActivity(intent); } - private void shareApp(DB.App app) { + private void shareApp(App app) { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); diff --git a/src/org/fdroid/fdroid/AppFilter.java b/src/org/fdroid/fdroid/AppFilter.java index 9e85f8eae..ff2be9a46 100644 --- a/src/org/fdroid/fdroid/AppFilter.java +++ b/src/org/fdroid/fdroid/AppFilter.java @@ -21,28 +21,25 @@ package org.fdroid.fdroid; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import org.fdroid.fdroid.data.App; public class AppFilter { - boolean pref_rooted; - - public AppFilter(Context ctx) { - - // Read preferences and cache them so we can do quick lookups. - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(ctx); - pref_rooted = prefs.getBoolean(Preferences.PREF_ROOTED, true); - } - // Return true if the given app should be filtered out based on user // preferences, and false otherwise. - public boolean filter(DB.App app) { - if (app.requirements == null) return false; + public boolean filter(App app) { + + boolean filterRequiringRoot = Preferences.get().filterAppsRequiringRoot(); + + if (app.requirements == null || !filterRequiringRoot) return false; + for (String r : app.requirements) { - if (r.equals("root") && !pref_rooted) + if (r.equals("root")) return true; } + return false; + } } diff --git a/src/org/fdroid/fdroid/AppListManager.java b/src/org/fdroid/fdroid/AppListManager.java deleted file mode 100644 index ee66e5736..000000000 --- a/src/org/fdroid/fdroid/AppListManager.java +++ /dev/null @@ -1,248 +0,0 @@ -package org.fdroid.fdroid; - -import java.util.*; - -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.util.Log; -import android.widget.ArrayAdapter; -import android.os.Build; - -import org.fdroid.fdroid.views.AppListAdapter; -import org.fdroid.fdroid.views.AvailableAppListAdapter; -import org.fdroid.fdroid.views.CanUpdateAppListAdapter; -import org.fdroid.fdroid.views.InstalledAppListAdapter; - -/** - * Should be owned by the FDroid Activity, but used by the AppListFragments. - * The idea is that it takes a non-trivial amount of time to work this stuff - * out, and it is quicker if we only do it once for each view, rather than - * each fragment figuring out their own list independently. - */ -public class AppListManager { - - private List allApps = null; - - private FDroid fdroidActivity; - - private AppListAdapter availableApps; - private AppListAdapter installedApps; - private AppListAdapter canUpgradeApps; - private ArrayAdapter categories; - - private String currentCategory = null; - private String categoryAll = null; - private String categoryWhatsNew = null; - private String categoryRecentlyUpdated = null; - - public AppListAdapter getAvailableAdapter() { - return availableApps; - } - - public AppListAdapter getInstalledAdapter() { - return installedApps; - } - - public AppListAdapter getCanUpdateAdapter() { - return canUpgradeApps; - } - - public ArrayAdapter getCategoriesAdapter() { - return categories; - } - - public AppListManager(FDroid activity) { - this.fdroidActivity = activity; - - availableApps = new AvailableAppListAdapter(fdroidActivity); - installedApps = new InstalledAppListAdapter(fdroidActivity); - canUpgradeApps = new CanUpdateAppListAdapter(fdroidActivity); - - // Needs to be created before createViews(), because that will use the - // getCategoriesAdapter() accessor which expects this object... - categories = new ArrayAdapter(activity, - android.R.layout.simple_spinner_item, new ArrayList()); - categories - .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - } - - private void clear() { - installedApps.clear(); - availableApps.clear(); - canUpgradeApps.clear(); - categories.clear(); - } - - private void notifyLists() { - // Tell the lists that the data behind the adapter has changed, so - // they can refresh... - availableApps.notifyDataSetChanged(); - installedApps.notifyDataSetChanged(); - canUpgradeApps.notifyDataSetChanged(); - categories.notifyDataSetChanged(); - } - - private void updateCategories() { - try { - DB db = DB.getDB(); - - // Populate the category list with the real categories, and the - // locally generated meta-categories for "All", "What's New" and - // "Recently Updated"... - categoryAll = fdroidActivity - .getString(R.string.category_all); - categoryWhatsNew = fdroidActivity - .getString(R.string.category_whatsnew); - categoryRecentlyUpdated = fdroidActivity - .getString(R.string.category_recentlyupdated); - - categories.add(categoryWhatsNew); - categories.add(categoryRecentlyUpdated); - categories.add(categoryAll); - if (Build.VERSION.SDK_INT >= 11) { - categories.addAll(db.getCategories()); - } else { - List categs = db.getCategories(); - for (String category : categs) { - categories.add(category); - } - } - - if (currentCategory == null) - currentCategory = categoryWhatsNew; - - } finally { - DB.releaseDB(); - } - } - - // Tell the FDroid activity to update its "Update (x)" tab to correctly - // reflect the number of updates available. - private void notifyActivity() { - fdroidActivity.refreshUpdateTabLabel(); - } - - public void repopulateLists() { - - long startTime = System.currentTimeMillis(); - - clear(); - - updateCategories(); - updateApps(); - notifyLists(); - notifyActivity(); - - Log.d("FDroid", "Updated lists - " + allApps.size() + " in total" - + " (update took " + (System.currentTimeMillis() - startTime) - + " ms)"); - } - - // Calculate the cutoff date we'll use for What's New and Recently - // Updated... - private Date calcMaxHistory() { - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(fdroidActivity.getBaseContext()); - String daysPreference = prefs.getString(Preferences.PREF_UPD_HISTORY, "14"); - int maxHistoryDays = Integer.parseInt(daysPreference); - Calendar recent = Calendar.getInstance(); - recent.add(Calendar.DAY_OF_YEAR, -maxHistoryDays); - return recent.getTime(); - } - - // recentDate could really be calculated here, but this is just a hack so - // it doesn't need to be calculated for every single app. The reason it - // isn't an instance variable is because the preferences may change, and - // we wouldn't know. - private boolean isInCategory(DB.App app, String category, Date recentDate) { - if (category.equals(categoryAll)) { - return true; - } - if (category.equals(categoryWhatsNew)) { - if (app.added == null) - return false; - if (app.added.compareTo(recentDate) < 0) - return false; - return true; - } - if (category.equals(categoryRecentlyUpdated)) { - if (app.lastUpdated == null) - return false; - // Don't include in the recently updated category if the - // 'update' was actually it being added. - if (app.lastUpdated.compareTo(app.added) == 0) - return false; - if (app.lastUpdated.compareTo(recentDate) < 0) - return false; - return true; - } - if (app.categories == null) return false; - return app.categories.contains(category); - } - - // Returns false if the app list is empty and the fdroid activity decided - // to attempt updating it. - private boolean updateApps() { - - allApps = ((FDroidApp)fdroidActivity.getApplication()).getApps(); - - if (allApps.isEmpty()) { - // If its the first time we've run the app, this should update - // the repos. If not, it will do nothing, presuming that the repos - // are invalid, the internet is stuffed, the sky has fallen, etc... - return fdroidActivity.updateEmptyRepos(); - } - - Date recentDate = calcMaxHistory(); - List availApps = new ArrayList(); - for (DB.App app : allApps) { - - // Add it to the list(s). Always to installed and updates, but - // only to available if it's not filtered. - if (isInCategory(app, currentCategory, recentDate)) { - availApps.add(app); - } - if (app.installedVersion != null) { - installedApps.addItem(app); - if (app.toUpdate) { - canUpgradeApps.addItem(app); - } - } - } - - if (currentCategory.equals(categoryWhatsNew)) { - Collections.sort(availApps, new WhatsNewComparator()); - } else if (currentCategory.equals(categoryRecentlyUpdated)) { - Collections.sort(availApps, new RecentlyUpdatedComparator()); - } - - availableApps.addItems(availApps); - - return true; - } - - public void setCurrentCategory(String currentCategory) { - if (!this.currentCategory.equals(currentCategory)){ - this.currentCategory = currentCategory; - repopulateLists(); - } - } - - public String getCurrentCategory() { - return this.currentCategory; - } - - static class WhatsNewComparator implements Comparator { - @Override - public int compare(DB.App lhs, DB.App rhs) { - return rhs.added.compareTo(lhs.added); - } - } - - static class RecentlyUpdatedComparator implements Comparator { - @Override - public int compare(DB.App lhs, DB.App rhs) { - return rhs.lastUpdated.compareTo(lhs.lastUpdated); - } - } -} diff --git a/src/org/fdroid/fdroid/CompatibilityChecker.java b/src/org/fdroid/fdroid/CompatibilityChecker.java new file mode 100644 index 000000000..5aa4ac31c --- /dev/null +++ b/src/org/fdroid/fdroid/CompatibilityChecker.java @@ -0,0 +1,103 @@ +package org.fdroid.fdroid; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.FeatureInfo; +import android.content.pm.PackageManager; +import android.preference.PreferenceManager; +import android.util.Log; +import org.fdroid.fdroid.compat.Compatibility; +import org.fdroid.fdroid.compat.SupportedArchitectures; +import org.fdroid.fdroid.data.Apk; + +import java.util.*; + +// Call getIncompatibleReasons(apk) on an instance of this class to + // find reasons why an apk may be incompatible with the user's device. +public class CompatibilityChecker extends Compatibility { + + private Context context; + private Set features; + private Set cpuAbis; + private String cpuAbisDesc; + private boolean ignoreTouchscreen; + + public CompatibilityChecker(Context ctx) { + + context = ctx.getApplicationContext(); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); + ignoreTouchscreen = prefs.getBoolean(Preferences.PREF_IGN_TOUCH, false); + + PackageManager pm = ctx.getPackageManager(); + StringBuilder logMsg = new StringBuilder(); + logMsg.append("Available device features:"); + features = new HashSet(); + if (pm != null) { + for (FeatureInfo fi : pm.getSystemAvailableFeatures()) { + features.add(fi.name); + logMsg.append('\n'); + logMsg.append(fi.name); + } + } + + cpuAbis = SupportedArchitectures.getAbis(); + + StringBuilder builder = new StringBuilder(); + boolean first = true; + for (String abi : cpuAbis) { + if (first) first = false; + else builder.append(", "); + builder.append(abi); + } + cpuAbisDesc = builder.toString(); + + Log.d("FDroid", logMsg.toString()); + } + + private boolean compatibleApi(Utils.CommaSeparatedList nativecode) { + if (nativecode == null) return true; + for (String abi : nativecode) { + if (cpuAbis.contains(abi)) { + return true; + } + } + return false; + } + + public List getIncompatibleReasons(final Apk apk) { + + List incompatibleReasons = new ArrayList(); + + if (!hasApi(apk.minSdkVersion)) { + incompatibleReasons.add( + context.getResources().getString( + R.string.minsdk_or_later, + Utils.getAndroidVersionName(apk.minSdkVersion))); + } + + if (apk.features != null) { + for (String feat : apk.features) { + if (ignoreTouchscreen + && feat.equals("android.hardware.touchscreen")) { + // Don't check it! + } else if (!features.contains(feat)) { + Collections.addAll(incompatibleReasons, feat.split(",")); + Log.d("FDroid", apk.id + " vercode " + apk.vercode + + " is incompatible based on lack of " + + feat); + } + } + } + if (!compatibleApi(apk.nativecode)) { + for (String code : apk.nativecode) { + incompatibleReasons.add(code); + } + Log.d("FDroid", apk.id + " vercode " + apk.vercode + + " only supports " + Utils.CommaSeparatedList.str(apk.nativecode) + + " while your architectures are " + cpuAbisDesc); + } + + return incompatibleReasons; + } +} \ No newline at end of file diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java deleted file mode 100644 index b101cad14..000000000 --- a/src/org/fdroid/fdroid/DB.java +++ /dev/null @@ -1,943 +0,0 @@ -/* - * Copyright (C) 2010-13 Ciaran Gultnieks, ciaran@ciarang.com - * Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -package org.fdroid.fdroid; - -import java.io.File; -import java.security.MessageDigest; -import java.net.MalformedURLException; -import java.net.URL; -import java.security.cert.Certificate; -import java.security.cert.CertificateEncodingException; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.Formatter; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.Semaphore; - -import android.content.ContentValues; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; -import android.content.pm.FeatureInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.preference.PreferenceManager; -import android.text.TextUtils; -import android.text.TextUtils.SimpleStringSplitter; -import android.util.DisplayMetrics; -import android.util.Log; - -import org.fdroid.fdroid.compat.Compatibility; -import org.fdroid.fdroid.compat.ContextCompat; -import org.fdroid.fdroid.compat.SupportedArchitectures; -import org.fdroid.fdroid.data.*; - -public class DB { - - private static Semaphore dbSync = new Semaphore(1, true); - private static DB dbInstance = null; - - // Initialise the database. Called once when the application starts up. - static void initDB(Context ctx) { - dbInstance = new DB(ctx); - } - - // Get access to the database. Must be called before any database activity, - // and releaseDB must be called subsequently. Returns null in the event of - // failure. - public static DB getDB() { - try { - dbSync.acquire(); - return dbInstance; - } catch (InterruptedException e) { - return null; - } - } - - // Release database access lock acquired via getDB(). - public static void releaseDB() { - dbSync.release(); - } - - // Possible values of the SQLite flag "synchronous" - public static final int SYNC_OFF = 0; - public static final int SYNC_NORMAL = 1; - public static final int SYNC_FULL = 2; - - private SQLiteDatabase db; - - public static final String TABLE_APP = "fdroid_app"; - - public static class App implements Comparable { - - public App() { - name = "Unknown"; - summary = "Unknown application"; - icon = null; - id = "unknown"; - license = "Unknown"; - detail_trackerURL = null; - detail_sourceURL = null; - detail_donateURL = null; - detail_bitcoinAddr = null; - detail_litecoinAddr = null; - detail_dogecoinAddr = null; - detail_webURL = null; - categories = null; - antiFeatures = null; - requirements = null; - hasUpdates = false; - toUpdate = false; - updated = false; - added = null; - lastUpdated = null; - apks = new ArrayList(); - detail_Populated = false; - compatible = false; - ignoreAllUpdates = false; - ignoreThisUpdate = 0; - filtered = false; - iconUrl = null; - } - - // True when all the detail fields are populated, False otherwise. - public boolean detail_Populated; - - // True if compatible with the device (i.e. if at least one apk is) - public boolean compatible; - - public String id; - public String name; - public String summary; - public String icon; - - // Null when !detail_Populated - public String detail_description; - - public String license; - - // Null when !detail_Populated - public String detail_webURL; - - // Null when !detail_Populated - public String detail_trackerURL; - - // Null when !detail_Populated - public String detail_sourceURL; - - // Donate link, or null - // Null when !detail_Populated - public String detail_donateURL; - - // Bitcoin donate address, or null - // Null when !detail_Populated - public String detail_bitcoinAddr; - - // Litecoin donate address, or null - // Null when !detail_Populated - public String detail_litecoinAddr; - - // Dogecoin donate address, or null - // Null when !detail_Populated - public String detail_dogecoinAddr; - - // Flattr donate ID, or null - // Null when !detail_Populated - public String detail_flattrID; - - public String curVersion; - public int curVercode; - public Apk curApk; - public Date added; - public Date lastUpdated; - - // Installed version (or null), version code and whether it was - // installed by the user or bundled with the system. These are valid - // only when getApps() has been called with getinstalledinfo=true. - public String installedVersion; - public int installedVerCode; - public boolean userInstalled; - - // List of categories (as defined in the metadata - // documentation) or null if there aren't any. - public CommaSeparatedList categories; - - // List of anti-features (as defined in the metadata - // documentation) or null if there aren't any. - public CommaSeparatedList antiFeatures; - - // List of special requirements (such as root privileges) or - // null if there aren't any. - public CommaSeparatedList requirements; - - // Whether the app is filtered or not based on AntiFeatures and root - // permission (set in the Settings page) - public boolean filtered; - - // True if there are new versions (apks) available, regardless of - // any filtering - public boolean hasUpdates; - - // True if there are new versions (apks) available and the user wants - // to be notified about them - public boolean toUpdate; - - // True if all updates for this app are to be ignored - public boolean ignoreAllUpdates; - - // True if the current update for this app is to be ignored - public int ignoreThisUpdate; - - // Used internally for tracking during repo updates. - public boolean updated; - - // List of apks. - public List apks; - - public String iconUrl; - - // Get the current version - this will be one of the Apks from 'apks'. - // Can return null if there are no available versions. - // This should be the 'current' version, as in the most recent stable - // one, that most users would want by default. It might not be the - // most recent, if for example there are betas etc. - public Apk getCurrentVersion() { - - // Try and return the real current version first. It will find the - // closest version smaller than the curVercode, being the same - // vercode if it exists. - if (curVercode > 0) { - int latestcode = -1; - Apk latestapk = null; - for (Apk apk : apks) { - if ((!this.compatible || apk.compatible) - && apk.vercode <= curVercode - && apk.vercode > latestcode) { - latestapk = apk; - latestcode = apk.vercode; - } - } - return latestapk; - } - - // If the current version was not set we return the most recent apk. - if (curVercode == -1) { - int latestcode = -1; - Apk latestapk = null; - for (Apk apk : apks) { - if ((!this.compatible || apk.compatible) - && apk.vercode > latestcode) { - latestapk = apk; - latestcode = apk.vercode; - } - } - return latestapk; - } - - return null; - } - - @Override - public int compareTo(App arg0) { - return name.compareToIgnoreCase(arg0.name); - } - - } - - public static String calcFingerprint(String keyHexString) { - if (TextUtils.isEmpty(keyHexString)) - return null; - else - return calcFingerprint(Hasher.unhex(keyHexString)); - } - - public static String calcFingerprint(Certificate cert) { - try { - return calcFingerprint(cert.getEncoded()); - } catch (CertificateEncodingException e) { - return null; - } - } - - public static String calcFingerprint(byte[] key) { - String ret = null; - try { - // keytool -list -v gives you the SHA-256 fingerprint - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - digest.update(key); - byte[] fingerprint = digest.digest(); - Formatter formatter = new Formatter(new StringBuilder()); - for (int i = 1; i < fingerprint.length; i++) { - formatter.format("%02X", fingerprint[i]); - } - ret = formatter.toString(); - formatter.close(); - } catch (Exception e) { - Log.w("FDroid", "Unable to get certificate fingerprint.\n" - + Log.getStackTraceString(e)); - } - return ret; - } - - /** - * Get the local storage (cache) path. This will also create it if - * it doesn't exist. It can return null if it's currently unavailable. - */ - public static File getDataPath(Context ctx) { - return ContextCompat.create(ctx).getExternalCacheDir(); - } - - private Context mContext; - private Apk.CompatibilityChecker compatChecker = null; - - // The date format used for storing dates (e.g. lastupdated, added) in the - // database. - public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); - - private DB(Context ctx) { - - mContext = ctx; - DBHelper h = new DBHelper(ctx); - db = h.getWritableDatabase(); - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(mContext); - String sync_mode = prefs.getString(Preferences.PREF_DB_SYNC, null); - if ("off".equals(sync_mode)) - setSynchronizationMode(SYNC_OFF); - else if ("normal".equals(sync_mode)) - setSynchronizationMode(SYNC_NORMAL); - else if ("full".equals(sync_mode)) - setSynchronizationMode(SYNC_FULL); - else - sync_mode = null; - if (sync_mode != null) - Log.d("FDroid", "Database synchronization mode: " + sync_mode); - } - - public void close() { - db.close(); - db = null; - } - - // Delete the database, which should cause it to be re-created next time - // it's used. - public static void delete(Context ctx) { - try { - ctx.deleteDatabase(DBHelper.DATABASE_NAME); - // Also try and delete the old one, from versions 0.13 and earlier. - ctx.deleteDatabase("fdroid_db"); - } catch (Exception ex) { - Log.e("FDroid", - "Exception in DB.delete:\n" + Log.getStackTraceString(ex)); - } - } - - public List getCategories() { - List result = new ArrayList(); - Cursor c = null; - try { - c = db.query(true, TABLE_APP, new String[] { "categories" }, - null, null, null, null, null, null); - c.moveToFirst(); - while (!c.isAfterLast()) { - CommaSeparatedList categories = CommaSeparatedList - .make(c.getString(0)); - if (categories != null) { - for (String category : categories) { - if (!result.contains(category)) { - result.add(category); - } - } - } - c.moveToNext(); - } - } catch (Exception e) { - Log.e("FDroid", - "Exception during database reading:\n" - + Log.getStackTraceString(e)); - } finally { - if (c != null) { - c.close(); - } - } - Collections.sort(result); - return result; - } - - private static final String[] POPULATE_APP_COLS = new String[] { - "description", "webURL", "trackerURL", "sourceURL", - "donateURL", "bitcoinAddr", "flattrID", "litecoinAddr", "dogecoinAddr" }; - - private void populateAppDetails(App app) { - Cursor cursor = null; - try { - cursor = db.query(TABLE_APP, POPULATE_APP_COLS, "id = ?", - new String[] { app.id }, null, null, null, null); - cursor.moveToFirst(); - app.detail_description = cursor.getString(0); - app.detail_webURL = cursor.getString(1); - app.detail_trackerURL = cursor.getString(2); - app.detail_sourceURL = cursor.getString(3); - app.detail_donateURL = cursor.getString(4); - app.detail_bitcoinAddr = cursor.getString(5); - app.detail_flattrID = cursor.getString(6); - app.detail_litecoinAddr = cursor.getString(7); - app.detail_dogecoinAddr = cursor.getString(8); - app.detail_Populated = true; - } catch (Exception e) { - Log.d("FDroid", "Error populating app details " + app.id ); - Log.d("FDroid", e.getMessage()); - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - private static final String[] POPULATE_APK_COLS = new String[] { - ApkProvider.DataColumns.HASH, - ApkProvider.DataColumns.HASH_TYPE, - ApkProvider.DataColumns.SIZE, - ApkProvider.DataColumns.PERMISSIONS - }; - - private void populateApkDetails(Apk apk, long repo) { - if (repo == 0 || repo == apk.repo) { - Apk loadedApk = ApkProvider.Helper.find( - mContext, apk.id, apk.vercode, POPULATE_APK_COLS); - apk.detail_hash = loadedApk.detail_hash; - apk.detail_hashType = loadedApk.detail_hashType; - apk.detail_size = loadedApk.detail_size; - apk.detail_permissions = loadedApk.detail_permissions; - } else { - Log.d("FDroid", "Not setting details for apk '" + apk.id + "' (version " + apk.version +") because it belongs to a different repo."); - } - } - - // Populate the details for the given app, if necessary. - // If 'apkrepo' is non-zero, only apks from that repo are - // populated (this is used during the update process) - public void populateDetails(App app, long apkRepo) { - if (!app.detail_Populated) { - populateAppDetails(app); - } - - for (Apk apk : app.apks) { - if (apk.detail_hash == null) { - populateApkDetails(apk, apkRepo); - } - } - } - - // Return a list of apps matching the given criteria. Filtering is - // also done based on compatibility and anti-features according to - // the user's current preferences. - public List getApps(boolean getinstalledinfo) { - - // If we're going to need it, get info in what's currently installed - Map systemApks = null; - if (getinstalledinfo) { - Log.d("FDroid", "Reading installed packages"); - systemApks = new HashMap(); - List installedPackages = mContext.getPackageManager() - .getInstalledPackages(0); - for (PackageInfo appInfo : installedPackages) { - systemApks.put(appInfo.packageName, appInfo); - } - } - - Map apps = new HashMap(); - Cursor c = null; - long startTime = System.currentTimeMillis(); - try { - - String cols[] = new String[] { "antiFeatures", "requirements", - "categories", "id", "name", "summary", "icon", "license", - "curVersion", "curVercode", "added", "lastUpdated", - "compatible", "ignoreAllUpdates", "ignoreThisUpdate" }; - c = db.query(TABLE_APP, cols, null, null, null, null, null); - c.moveToFirst(); - while (!c.isAfterLast()) { - - App app = new App(); - app.antiFeatures = DB.CommaSeparatedList.make(c.getString(0)); - app.requirements = DB.CommaSeparatedList.make(c.getString(1)); - app.categories = DB.CommaSeparatedList.make(c.getString(2)); - app.id = c.getString(3); - app.name = c.getString(4); - app.summary = c.getString(5); - app.icon = c.getString(6); - app.license = c.getString(7); - app.curVersion = c.getString(8); - app.curVercode = c.getInt(9); - String sAdded = c.getString(10); - app.added = (sAdded == null || sAdded.length() == 0) ? null - : DATE_FORMAT.parse(sAdded); - String sLastUpdated = c.getString(11); - app.lastUpdated = (sLastUpdated == null || sLastUpdated - .length() == 0) ? null : DATE_FORMAT - .parse(sLastUpdated); - app.compatible = c.getInt(12) == 1; - app.ignoreAllUpdates = c.getInt(13) == 1; - app.ignoreThisUpdate = c.getInt(14); - app.hasUpdates = false; - - if (getinstalledinfo && systemApks.containsKey(app.id)) { - PackageInfo sysapk = systemApks.get(app.id); - app.installedVersion = sysapk.versionName; - if (app.installedVersion == null) - app.installedVersion = "null"; - app.installedVerCode = sysapk.versionCode; - if (sysapk.applicationInfo != null) { - app.userInstalled = ((sysapk.applicationInfo.flags - & ApplicationInfo.FLAG_SYSTEM) != 1); - } - } else { - app.installedVersion = null; - app.installedVerCode = 0; - app.userInstalled = false; - } - - apps.put(app.id, app); - - c.moveToNext(); - } - c.close(); - c = null; - - Log.d("FDroid", "Read app data from database " + " (took " - + (System.currentTimeMillis() - startTime) + " ms)"); - - DisplayMetrics metrics = mContext.getResources() - .getDisplayMetrics(); - String iconsDir = null; - if (metrics.densityDpi >= 640) { - iconsDir = "/icons-640/"; - } else if (metrics.densityDpi >= 480) { - iconsDir = "/icons-480/"; - } else if (metrics.densityDpi >= 320) { - iconsDir = "/icons-320/"; - } else if (metrics.densityDpi >= 240) { - iconsDir = "/icons-240/"; - } else if (metrics.densityDpi >= 160) { - iconsDir = "/icons-160/"; - } else { - iconsDir = "/icons-120/"; - } - metrics = null; - Log.d("FDroid", "Density-specific icons dir is " + iconsDir); - - List apks = ApkProvider.Helper.all(mContext); - for (Apk apk : apks) { - App app = apps.get(apk.id); - if (app == null) { - continue; - } - app.apks.add(apk); - if (app.iconUrl == null && app.icon != null) { - if (apk.repoVersion >= 11) { - app.iconUrl = apk.repoAddress + iconsDir + app.icon; - } else { - app.iconUrl = apk.repoAddress + "/icons/" + app.icon; - } - } - } - - } catch (Exception e) { - Log.e("FDroid", - "Exception during database reading:\n" - + Log.getStackTraceString(e)); - } finally { - if (c != null) { - c.close(); - } - - Log.d("FDroid", "Read app and apk data from database " + " (took " - + (System.currentTimeMillis() - startTime) + " ms)"); - } - - List result = new ArrayList(apps.values()); - Collections.sort(result); - - // Fill in the hasUpdates fields if we have the necessary information... - if (getinstalledinfo) { - - // We'll say an application has updates if it's installed AND the - // version is older than the current one - for (App app : result) { - app.curApk = app.getCurrentVersion(); - if (app.curApk != null - && app.installedVerCode > 0 - && app.installedVerCode < app.curApk.vercode) { - app.hasUpdates = true; - } - } - } - - return result; - } - - - // Alternative to getApps() that only refreshes the installation details - // of those apps in invalidApps. Much faster when returning from - // installs/uninstalls, where getApps() was already called before. - public List refreshApps(List apps, List invalidApps) { - - List installedPackages = mContext.getPackageManager() - .getInstalledPackages(0); - long startTime = System.currentTimeMillis(); - List refreshedApps = new ArrayList(); - for (String appid : invalidApps) { - if (refreshedApps.contains(appid)) continue; - App app = null; - int index = -1; - for (App oldapp : apps) { - index++; - if (oldapp.id.equals(appid)) { - app = oldapp; - break; - } - } - - if (app == null) continue; - - PackageInfo installed = null; - - for (PackageInfo appInfo : installedPackages) { - if (appInfo.packageName.equals(appid)) { - installed = appInfo; - break; - } - } - - if (installed != null) { - app.installedVersion = installed.versionName; - if (app.installedVersion == null) - app.installedVersion = "null"; - app.installedVerCode = installed.versionCode; - } else { - app.installedVersion = null; - app.installedVerCode = 0; - } - - app.hasUpdates = false; - app.curApk = app.getCurrentVersion(); - if (app.curApk != null - && app.installedVersion != null - && app.installedVerCode < app.curApk.vercode) { - app.hasUpdates = true; - } - - apps.set(index, app); - refreshedApps.add(appid); - } - Log.d("FDroid", "Refreshing " + refreshedApps.size() + " apps took " - + (System.currentTimeMillis() - startTime) + " ms"); - - return apps; - } - - public List doSearch(String query) { - - List ids = new ArrayList(); - Cursor c = null; - try { - String filter = "%" + query + "%"; - c = db.query(TABLE_APP, new String[] { "id" }, - "id like ? or name like ? or summary like ? or description like ?", - new String[] { filter, filter, filter, filter }, null, null, null); - c.moveToFirst(); - while (!c.isAfterLast()) { - ids.add(c.getString(0)); - c.moveToNext(); - } - } finally { - if (c != null) - c.close(); - } - return ids; - } - - public static class CommaSeparatedList implements Iterable { - private String value; - - private CommaSeparatedList(String list) { - value = list; - } - - public static CommaSeparatedList make(String list) { - if (list == null || list.length() == 0) - return null; - else - return new CommaSeparatedList(list); - } - - public static String str(CommaSeparatedList instance) { - return (instance == null ? null : instance.toString()); - } - - @Override - public String toString() { - return value; - } - - @Override - public Iterator iterator() { - SimpleStringSplitter splitter = new SimpleStringSplitter(','); - splitter.setString(value); - return splitter.iterator(); - } - - public boolean contains(String v) { - for (String s : this) { - if (s.equals(v)) - return true; - } - return false; - } - } - - private List updateApps = null; - - // Called before a repo update starts. - public void beginUpdate(List apps) { - // Get a list of all apps. All the apps and apks in this list will - // have 'updated' set to false at this point, and we will only set - // it to true when we see the app/apk in a repository. Thus, at the - // end, any that are still false can be removed. - updateApps = apps; - Log.d("FDroid", "AppUpdate: " + updateApps.size() + " apps before starting."); - // Wrap the whole update in a transaction. Make sure to call - // either endUpdate or cancelUpdate to commit or discard it, - // respectively. - db.beginTransaction(); - } - - // Called when a repo update ends. Any applications that have not been - // updated (by a call to updateApplication) are assumed to be no longer - // in the repos. - public void endUpdate() { - if (updateApps == null) - return; - Log.d("FDroid", "Processing endUpdate - " + updateApps.size() - + " apps before"); - for (App app : updateApps) { - if (!app.updated) { - // The application hasn't been updated, so it's no longer - // in the repos. - Log.d("FDroid", "AppUpdate: " + app.name - + " is no longer in any repository - removing"); - db.delete(TABLE_APP, "id = ?", new String[]{app.id}); - ApkProvider.Helper.deleteApksByApp(mContext, app); - } else { - for (Apk apk : app.apks) { - if (!apk.updated) { - // The package hasn't been updated, so this is a - // version that's no longer available. - Log.d("FDroid", "AppUpdate: Package " + apk.id + "/" - + apk.version - + " is no longer in any repository - removing"); - ApkProvider.Helper.delete(mContext, app.id, apk.vercode); - } - } - } - } - // Commit updates to the database. - db.setTransactionSuccessful(); - db.endTransaction(); - Log.d("FDroid", "AppUpdate: " + updateApps.size() - + " apps on completion."); - updateApps = null; - } - - // Called instead of endUpdate if the update failed. - public void cancelUpdate() { - if (updateApps != null) { - db.endTransaction(); - updateApps = null; - } - } - - // Called during update to supply new details for an application (or - // details of a completely new one). Calls to this must be wrapped by - // a call to beginUpdate and a call to endUpdate. - public void updateApplication(App upapp) { - - if (updateApps == null) { - return; - } - - // Lazy initialise this... - if (compatChecker == null) { - compatChecker = new Apk.CompatibilityChecker(mContext); - } - - // See if it's compatible (by which we mean if it has at least one - // compatible apk) - upapp.compatible = false; - for (Apk apk : upapp.apks) { - if (compatChecker.isCompatible(apk)) { - apk.compatible = true; - upapp.compatible = true; - } - } - - boolean found = false; - for (App app : updateApps) { - if (app.id.equals(upapp.id)) { - updateApp(app, upapp); - app.updated = true; - found = true; - for (Apk upapk : upapp.apks) { - boolean afound = false; - for (Apk apk : app.apks) { - if (apk.vercode == upapk.vercode) { - - ApkProvider.Helper.update( - mContext, - upapk, - apk.id, - apk.vercode); - - apk.updated = true; - afound = true; - break; - } - } - if (!afound) { - // A new version of this application. - ApkProvider.Helper.insert(mContext, upapk); - upapk.updated = true; - app.apks.add(upapk); - } - } - break; - } - } - if (!found) { - // It's a brand new application... - updateApp(null, upapp); - for (Apk upapk : upapp.apks) { - ApkProvider.Helper.insert(mContext, upapk); - upapk.updated = true; - } - upapp.updated = true; - updateApps.add(upapp); - } - - } - - // Update application details in the database. - // 'oldapp' - previous details - i.e. what's in the database. - // If null, this app is not in the database at all and - // should be added. - // 'upapp' - updated details - private void updateApp(App oldapp, App upapp) { - ContentValues values = new ContentValues(); - values.put("id", upapp.id); - values.put("name", upapp.name); - values.put("summary", upapp.summary); - values.put("icon", upapp.icon); - values.put("description", upapp.detail_description); - values.put("license", upapp.license); - values.put("webURL", upapp.detail_webURL); - values.put("trackerURL", upapp.detail_trackerURL); - values.put("sourceURL", upapp.detail_sourceURL); - values.put("donateURL", upapp.detail_donateURL); - values.put("bitcoinAddr", upapp.detail_bitcoinAddr); - values.put("litecoinAddr", upapp.detail_litecoinAddr); - values.put("dogecoinAddr", upapp.detail_dogecoinAddr); - values.put("flattrID", upapp.detail_flattrID); - values.put("added", - upapp.added == null ? "" : DATE_FORMAT.format(upapp.added)); - values.put( - "lastUpdated", - upapp.added == null ? "" : DATE_FORMAT - .format(upapp.lastUpdated)); - values.put("curVersion", upapp.curVersion); - values.put("curVercode", upapp.curVercode); - values.put("categories", CommaSeparatedList.str(upapp.categories)); - values.put("antiFeatures", CommaSeparatedList.str(upapp.antiFeatures)); - values.put("requirements", CommaSeparatedList.str(upapp.requirements)); - values.put("compatible", upapp.compatible ? 1 : 0); - - // Values to keep if already present - if (oldapp == null) { - values.put("ignoreAllUpdates", upapp.ignoreAllUpdates ? 1 : 0); - values.put("ignoreThisUpdate", upapp.ignoreThisUpdate); - } else { - values.put("ignoreAllUpdates", oldapp.ignoreAllUpdates ? 1 : 0); - values.put("ignoreThisUpdate", oldapp.ignoreThisUpdate); - } - - if (oldapp != null) { - db.update(TABLE_APP, values, "id = ?", new String[] { oldapp.id }); - } else { - db.insert(TABLE_APP, null, values); - } - } - - public void setIgnoreUpdates(String appid, boolean All, int This) { - db.execSQL("update " + TABLE_APP + " set" - + " ignoreAllUpdates=" + (All ? '1' : '0') - + ", ignoreThisUpdate="+This - + " where id = ?", new String[] { appid }); - } - - public void purgeApps(Repo repo, FDroidApp fdroid) { - db.beginTransaction(); - - try { - ApkProvider.Helper.deleteApksByRepo(mContext, repo); - List apps = getApps(false); - for (App app : apps) { - if (app.apks.isEmpty()) { - db.delete(TABLE_APP, "id = ?", new String[] { app.id }); - } - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - fdroid.invalidateAllApps(); - } - - public int getSynchronizationMode() { - Cursor cursor = db.rawQuery("PRAGMA synchronous", null); - cursor.moveToFirst(); - int mode = cursor.getInt(0); - cursor.close(); - return mode; - } - - public void setSynchronizationMode(int mode) { - db.execSQL("PRAGMA synchronous = " + mode); - } -} diff --git a/src/org/fdroid/fdroid/Downloader.java b/src/org/fdroid/fdroid/Downloader.java index ebd45271c..22bcac17e 100644 --- a/src/org/fdroid/fdroid/Downloader.java +++ b/src/org/fdroid/fdroid/Downloader.java @@ -108,8 +108,8 @@ public class Downloader extends Thread { // See if we already have this apk cached... if (localfile.exists()) { // We do - if its hash matches, we'll use it... - Hasher hash = new Hasher(curapk.detail_hashType, localfile); - if (hash.match(curapk.detail_hash)) { + Hasher hash = new Hasher(curapk.hashType, localfile); + if (hash.match(curapk.hash)) { Log.d("FDroid", "Using cached apk at " + localfile); synchronized (this) { progress = 1; @@ -130,7 +130,7 @@ public class Downloader extends Thread { synchronized (this) { filename = remotefile; progress = 0; - max = curapk.detail_size; + max = curapk.size; status = Status.RUNNING; } @@ -159,11 +159,11 @@ public class Downloader extends Thread { } return; } - Hasher hash = new Hasher(curapk.detail_hashType, localfile); - if (!hash.match(curapk.detail_hash)) { + Hasher hash = new Hasher(curapk.hashType, localfile); + if (!hash.match(curapk.hash)) { synchronized (this) { Log.d("FDroid", "Downloaded file hash of " + hash.getHash() - + " did not match repo's " + curapk.detail_hash); + + " did not match repo's " + curapk.hash); // No point keeping a bad file, whether we're // caching or not. localfile.delete(); diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index 9ef40383e..e54b8ec25 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -25,9 +25,11 @@ import android.app.NotificationManager; import android.content.*; import android.content.pm.PackageInfo; import android.content.res.Configuration; +import android.database.ContentObserver; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.Handler; import android.util.Log; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; @@ -41,6 +43,7 @@ import android.support.v4.view.MenuItemCompat; import android.support.v4.view.ViewPager; import org.fdroid.fdroid.compat.TabManager; +import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.views.AppListFragmentPageAdapter; public class FDroid extends FragmentActivity { @@ -58,21 +61,14 @@ public class FDroid extends FragmentActivity { private ViewPager viewPager; - private AppListManager manager = null; - private TabManager tabManager = null; - public AppListManager getManager() { - return manager; - } - @Override protected void onCreate(Bundle savedInstanceState) { ((FDroidApp) getApplication()).applyTheme(this); super.onCreate(savedInstanceState); - manager = new AppListManager(this); setContentView(R.layout.fdroid); createViews(); getTabManager().createTabs(); @@ -99,21 +95,9 @@ public class FDroid extends FragmentActivity { call.putExtra("appid", appid); startActivityForResult(call, REQUEST_APPDETAILS); } - } - @Override - protected void onResume() { - super.onResume(); - repopulateViews(); - } - - /** - * Must be done *after* createViews, because it will involve a - * callback to update the tab label for the "update" tab. This - * will fail unless the tabs have actually been created. - */ - protected void repopulateViews() { - manager.repopulateLists(); + Uri uri = AppProvider.getContentUri(); + getContentResolver().registerContentObserver(uri, true, new AppObserver()); } @Override @@ -253,8 +237,6 @@ public class FDroid extends FragmentActivity { if ((resultCode & PreferencesActivity.RESULT_RELOAD) != 0) { ((FDroidApp) getApplication()).invalidateAllApps(); - } else if ((resultCode & PreferencesActivity.RESULT_REFILTER) != 0) { - ((FDroidApp) getApplication()).filterApps(); } if ((resultCode & PreferencesActivity.RESULT_RESTART) != 0) { @@ -308,14 +290,7 @@ 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() { - UpdateService.updateNow(this).setListener(new ProgressListener() { - @Override - public void onProgress(Event event) { - if (event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES){ - repopulateViews(); - } - } - }); + UpdateService.updateNow(this); } private TabManager getTabManager() { @@ -335,4 +310,27 @@ public class FDroid extends FragmentActivity { nMgr.cancel(id); } + private class AppObserver extends ContentObserver { + + public AppObserver() { + super(null); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + FDroid.this.runOnUiThread(new Runnable() { + @Override + public void run() { + refreshUpdateTabLabel(); + } + }); + } + + @Override + public void onChange(boolean selfChange) { + onChange(selfChange, null); + } + + } + } diff --git a/src/org/fdroid/fdroid/FDroidApp.java b/src/org/fdroid/fdroid/FDroidApp.java index cd4e0179b..13f01829b 100644 --- a/src/org/fdroid/fdroid/FDroidApp.java +++ b/src/org/fdroid/fdroid/FDroidApp.java @@ -38,23 +38,19 @@ import android.app.Application; import android.content.Context; import android.content.SharedPreferences; -import org.fdroid.fdroid.Utils; - -import android.graphics.Bitmap; import android.preference.PreferenceManager; import android.util.Log; import com.nostra13.universalimageloader.cache.disc.impl.LimitedAgeDiscCache; -import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiscCache; import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator; -import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; -import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; import com.nostra13.universalimageloader.utils.StorageUtils; import de.duenndns.ssl.MemorizingTrustManager; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; import org.thoughtcrime.ssl.pinning.PinningTrustManager; import org.thoughtcrime.ssl.pinning.SystemKeyStore; @@ -90,6 +86,20 @@ public class FDroidApp extends Application { // it is more deterministic as to when this gets called... Preferences.setup(this); + // Set this up here, and the testing framework will override it when + // it gets fired up. + Utils.setupInstalledApkCache(new Utils.InstalledApkCache()); + + // If the user changes the preference to do with filtering rooted apps, + // it is easier to just notify a change in the app provider, + // so that the newly updated list will correctly filter relevant apps. + Preferences.get().registerAppsRequiringRootChangeListener(new Preferences.ChangeListener() { + @Override + public void onPreferenceChange() { + getContentResolver().notifyChange(AppProvider.getContentUri(), null); + } + }); + // Clear cached apk files. We used to just remove them after they'd // been installed, but this causes problems for proprietary gapps // users since the introduction of verification (on pre-4.2 Android), @@ -117,7 +127,6 @@ public class FDroidApp extends Application { apps = null; invalidApps = new ArrayList(); ctx = getApplicationContext(); - DB.initDB(ctx); UpdateService.schedule(ctx); ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(ctx) @@ -179,7 +188,7 @@ public class FDroidApp extends Application { private Context ctx; // Global list of all known applications. - private List apps; + private List apps; // Set when something has changed (database or installed apps) so we know // we should invalidate the apps. @@ -206,59 +215,4 @@ public class FDroidApp extends Application { invalidApps.add(id); } - // Get a list of all known applications. Should not be called when the - // database is locked (i.e. between DB.getDB() and db.releaseDB(). The - // contents should never be modified, it's for reading only. - public List getApps() { - - boolean invalid = false; - try { - appsInvalidLock.acquire(); - invalid = appsAllInvalid; - if (invalid) { - appsAllInvalid = false; - Log.d("FDroid", "Dropping cached app data"); - } - } catch (InterruptedException e) { - // Don't care - } finally { - appsInvalidLock.release(); - } - - if (apps == null || invalid) { - try { - DB db = DB.getDB(); - apps = db.getApps(true); - - } finally { - DB.releaseDB(); - } - } else if (!invalidApps.isEmpty()) { - try { - DB db = DB.getDB(); - apps = db.refreshApps(apps, invalidApps); - - invalidApps.clear(); - } finally { - DB.releaseDB(); - } - } - if (apps == null) - return new ArrayList(); - filterApps(); - return apps; - } - - public void filterApps() { - AppFilter appFilter = new AppFilter(ctx); - for (DB.App app : apps) { - app.filtered = appFilter.filter(app); - - app.toUpdate = (app.hasUpdates - && !app.ignoreAllUpdates - && app.curApk.vercode > app.ignoreThisUpdate - && !app.filtered); - } - } - } diff --git a/src/org/fdroid/fdroid/ManageRepo.java b/src/org/fdroid/fdroid/ManageRepo.java index 0bf667696..519ef2b80 100644 --- a/src/org/fdroid/fdroid/ManageRepo.java +++ b/src/org/fdroid/fdroid/ManageRepo.java @@ -21,13 +21,11 @@ package org.fdroid.fdroid; import android.app.Activity; import android.app.AlertDialog; -import android.content.ContentValues; -import android.content.Context; -import android.content.DialogInterface; +import android.content.*; +import android.preference.PreferenceManager; import android.support.v4.app.FragmentActivity; import android.support.v4.app.ListFragment; import android.support.v4.content.CursorLoader; -import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.net.wifi.WifiInfo; @@ -38,6 +36,7 @@ import android.support.v4.app.NavUtils; import android.support.v4.content.Loader; import android.support.v4.view.MenuItemCompat; import android.text.TextUtils; +import android.text.format.DateFormat; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; @@ -55,6 +54,7 @@ import org.fdroid.fdroid.views.fragments.RepoDetailsFragment; import java.net.MalformedURLException; import java.net.URL; +import java.util.Date; import java.util.Locale; public class ManageRepo extends FragmentActivity { @@ -178,7 +178,7 @@ class RepoListFragment extends ListFragment changed = true; } else { FDroidApp app = (FDroidApp)getActivity().getApplication(); - RepoProvider.Helper.purgeApps(repo, app); + RepoProvider.Helper.purgeApps(getActivity(), repo, app); String notification = getString(R.string.repo_disabled_notification, repo.name); Toast.makeText(getActivity(), notification, Toast.LENGTH_LONG).show(); } @@ -200,6 +200,43 @@ class RepoListFragment extends ListFragment */ private boolean isImportingRepo = false; + private View createHeaderView() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + TextView textLastUpdate = new TextView(getActivity()); + long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0); + String lastUpdateCheck = ""; + if (lastUpdate == 0) { + lastUpdateCheck = getString(R.string.never); + } else { + Date d = new Date(lastUpdate); + lastUpdateCheck = DateFormat.getDateFormat(getActivity()).format(d) + + " " + DateFormat.getTimeFormat(getActivity()).format(d); + } + textLastUpdate.setText(getString(R.string.last_update_check, lastUpdateCheck)); + + int sidePadding = (int)getResources().getDimension(R.dimen.padding_side); + int topPadding = (int)getResources().getDimension(R.dimen.padding_top); + + textLastUpdate.setPadding(sidePadding, topPadding, sidePadding, topPadding); + return textLastUpdate; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + // Can't do this in the onCreate view, because "onCreateView" which + // returns the list view is "called between onCreate and + // onActivityCreated" according to the docs. + getListView().addHeaderView(createHeaderView()); + + // This could go in onCreate (and used to) but it needs to be called + // after addHeaderView, which can only be called after onCreate... + repoAdapter = new RepoAdapter(getActivity(), null); + repoAdapter.setEnabledListener(this); + setListAdapter(repoAdapter); + } + @Override public void onCreate(Bundle savedInstanceState) { @@ -207,29 +244,6 @@ class RepoListFragment extends ListFragment setHasOptionsMenu(true); - repoAdapter = new RepoAdapter(getActivity(), null); - repoAdapter.setEnabledListener(this); - setListAdapter(repoAdapter); - - /* - TODO: Find some other way to display this info, now that we use the ListView widgets... - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(getBaseContext()); - - TextView tv_lastCheck = (TextView)findViewById(R.id.lastUpdateCheck); - long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0); - String s_lastUpdateCheck = ""; - if (lastUpdate == 0) { - s_lastUpdateCheck = getString(R.string.never); - } else { - Date d = new Date(lastUpdate); - s_lastUpdateCheck = DateFormat.getDateFormat(this).format(d) + - " " + DateFormat.getTimeFormat(this).format(d); - } - tv_lastCheck.setText(getString(R.string.last_update_check,s_lastUpdateCheck)); - - */ - /* let's see if someone is trying to send us a new repo */ Intent intent = getActivity().getIntent(); /* an URL from a click, NFC, QRCode scan, etc */ @@ -342,9 +356,12 @@ class RepoListFragment extends ListFragment 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) { // No need to prompt to update any more, we just did it! changed = false; } + } }); } diff --git a/src/org/fdroid/fdroid/PackageReceiver.java b/src/org/fdroid/fdroid/PackageReceiver.java index 180aa3f8c..c58bf99ff 100644 --- a/src/org/fdroid/fdroid/PackageReceiver.java +++ b/src/org/fdroid/fdroid/PackageReceiver.java @@ -30,6 +30,7 @@ public class PackageReceiver extends BroadcastReceiver { String appid = intent.getData().getSchemeSpecificPart(); Log.d("FDroid", "PackageReceiver received "+appid); ((FDroidApp) ctx.getApplicationContext()).invalidateApp(appid); + Utils.clearInstalledApksCache(); } } diff --git a/src/org/fdroid/fdroid/Preferences.java b/src/org/fdroid/fdroid/Preferences.java index 9c64b4717..de5695340 100644 --- a/src/org/fdroid/fdroid/Preferences.java +++ b/src/org/fdroid/fdroid/Preferences.java @@ -1,10 +1,8 @@ package org.fdroid.fdroid; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import android.app.LoaderManager; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; @@ -38,15 +36,20 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi public static final String PREF_IGN_TOUCH = "ignoreTouchscreen"; public static final String PREF_CACHE_APK = "cacheDownloaded"; public static final String PREF_EXPERT = "expert"; - public static final String PREF_DB_SYNC = "dbSyncMode"; public static final String PREF_UPD_LAST = "lastUpdateCheck"; private static final boolean DEFAULT_COMPACT_LAYOUT = false; + private static final boolean DEFAULT_ROOTED = true; + private static final int DEFAULT_UPD_HISTORY = 14; private boolean compactLayout = DEFAULT_COMPACT_LAYOUT; + private boolean filterAppsRequiringRoot = DEFAULT_ROOTED; private Map initialized = new HashMap(); + private List compactLayoutListeners = new ArrayList(); + private List filterAppsRequiringRootListeners = new ArrayList(); + private List updateHistoryListeners = new ArrayList(); private boolean isInitialized(String key) { return initialized.containsKey(key) && initialized.get(key); @@ -76,6 +79,45 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi compactLayoutListeners.remove(listener); } + /** + * Calculate the cutoff date we'll use for What's New and Recently + * Updated... + */ + public Date calcMaxHistory() { + String daysString = preferences.getString(PREF_UPD_HISTORY, Integer.toString(DEFAULT_UPD_HISTORY)); + int maxHistoryDays; + try { + maxHistoryDays = Integer.parseInt(daysString); + } catch (NumberFormatException e) { + maxHistoryDays = DEFAULT_UPD_HISTORY; + } + Calendar recent = Calendar.getInstance(); + recent.add(Calendar.DAY_OF_YEAR, -maxHistoryDays); + return recent.getTime(); + } + + /** + * This is cached as it is called several times inside the AppListAdapter. + * Providing it here means sthe shared preferences file only needs to be + * read once, and we will keep our copy up to date by listening to changes + * in PREF_ROOTED. + */ + public boolean filterAppsRequiringRoot() { + if (!isInitialized(PREF_ROOTED)) { + initialize(PREF_ROOTED); + filterAppsRequiringRoot = preferences.getBoolean(PREF_ROOTED, DEFAULT_ROOTED); + } + return filterAppsRequiringRoot; + } + + public void registerAppsRequiringRootChangeListener(ChangeListener listener) { + filterAppsRequiringRootListeners.add(listener); + } + + public void unregisterAppsRequiringRootChangeListener(ChangeListener listener) { + filterAppsRequiringRootListeners.remove(listener); + } + @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Log.d("FDroid", "Invalidating preference '" + key + "'."); @@ -85,9 +127,29 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi for ( ChangeListener listener : compactLayoutListeners ) { listener.onPreferenceChange(); } + } else if (key.equals(PREF_ROOTED)) { + for ( ChangeListener listener : filterAppsRequiringRootListeners ) { + listener.onPreferenceChange(); + } + } else if (key.equals(PREF_UPD_HISTORY)) { + for ( ChangeListener listener : updateHistoryListeners ) { + listener.onPreferenceChange(); + } } } + public void registerUpdateHistoryListener(ChangeListener listener) { + updateHistoryListeners.add(listener); + } + + public void unregisterUpdateHistoryListener(ChangeListener listener) { + updateHistoryListeners.remove(listener); + } + + public static interface ChangeListener { + public void onPreferenceChange(); + } + private static Preferences instance; public static void setup(Context context) { @@ -110,8 +172,4 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi return instance; } - public static interface ChangeListener { - public void onPreferenceChange(); - } - } diff --git a/src/org/fdroid/fdroid/PreferencesActivity.java b/src/org/fdroid/fdroid/PreferencesActivity.java index f66066d76..8de8a4589 100644 --- a/src/org/fdroid/fdroid/PreferencesActivity.java +++ b/src/org/fdroid/fdroid/PreferencesActivity.java @@ -37,7 +37,6 @@ public class PreferencesActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener { public static final int RESULT_RELOAD = 1; - public static final int RESULT_REFILTER = 2; public static final int RESULT_RESTART = 4; private int result = 0; @@ -53,8 +52,7 @@ public class PreferencesActivity extends PreferenceActivity implements Preferences.PREF_COMPACT_LAYOUT, Preferences.PREF_IGN_TOUCH, Preferences.PREF_CACHE_APK, - Preferences.PREF_EXPERT, - Preferences.PREF_DB_SYNC + Preferences.PREF_EXPERT }; @Override @@ -133,10 +131,6 @@ public class PreferencesActivity extends PreferenceActivity implements } else if (key.equals(Preferences.PREF_ROOTED)) { onoffSummary(key, R.string.rooted_on, R.string.rooted_off); - if (changing) { - result ^= RESULT_REFILTER; - setResult(result); - } } else if (key.equals(Preferences.PREF_IGN_TOUCH)) { onoffSummary(key, R.string.ignoreTouch_on, @@ -150,8 +144,6 @@ public class PreferencesActivity extends PreferenceActivity implements onoffSummary(key, R.string.expert_on, R.string.expert_off); - } else if (key.equals(Preferences.PREF_DB_SYNC)) { - entrySummary(key); } } diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java index a22c98c36..5f0e2e046 100644 --- a/src/org/fdroid/fdroid/RepoXMLHandler.java +++ b/src/org/fdroid/fdroid/RepoXMLHandler.java @@ -21,6 +21,7 @@ package org.fdroid.fdroid; import android.os.Bundle; import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.updater.RepoUpdater; import org.xml.sax.Attributes; @@ -28,19 +29,17 @@ import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import java.text.ParseException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; public class RepoXMLHandler extends DefaultHandler { // The repo we're processing. private Repo repo; - private Map apps; - private List appsList; + private List apps = new ArrayList(); + private List apksList = new ArrayList(); - private DB.App curapp = null; + private App curapp = null; private Apk curapk = null; private StringBuilder curchars = new StringBuilder(); @@ -64,17 +63,22 @@ public class RepoXMLHandler extends DefaultHandler { private int totalAppCount; - public RepoXMLHandler(Repo repo, List appsList, ProgressListener listener) { + public RepoXMLHandler(Repo repo, ProgressListener listener) { this.repo = repo; - this.apps = new HashMap(); - for (DB.App app : appsList) this.apps.put(app.id, app); - this.appsList = appsList; pubkey = null; name = null; description = null; progressListener = listener; } + public List getApps() { + return apps; + } + + public List getApks() { + return apksList; + } + public int getMaxAge() { return maxage; } public int getVersion() { return version; } @@ -104,21 +108,18 @@ public class RepoXMLHandler extends DefaultHandler { } if (curel.equals("application") && curapp != null) { - - // If we already have this application (must be from scanning a - // different repo) then just merge in the apks. - DB.App app = apps.get(curapp.id); - if (app != null) { - app.apks.addAll(curapp.apks); - } else { - appsList.add(curapp); - apps.put(curapp.id, curapp); - } - + apps.add(curapp); curapp = null; - + // If the app id is already present in this apps list, then it + // means the same index file has a duplicate app, which should + // not be allowed. + // However, I'm thinking that it should be unefined behaviour, + // because it is probably a bug in the fdroid server that made it + // happen, and I don't *think* it will crash the client, because + // the first app will insert, the second one will update the newly + // inserted one. } else if (curel.equals("package") && curapk != null && curapp != null) { - curapp.apks.add(curapk); + apksList.add(curapk); curapk = null; } else if (curapk != null && str != null) { if (curel.equals("version")) { @@ -131,19 +132,19 @@ public class RepoXMLHandler extends DefaultHandler { } } else if (curel.equals("size")) { try { - curapk.detail_size = Integer.parseInt(str); + curapk.size = Integer.parseInt(str); } catch (NumberFormatException ex) { - curapk.detail_size = 0; + curapk.size = 0; } } else if (curel.equals("hash")) { if (hashType == null || hashType.equals("md5")) { - if (curapk.detail_hash == null) { - curapk.detail_hash = str; - curapk.detail_hashType = "MD5"; + if (curapk.hash == null) { + curapk.hash = str; + curapk.hashType = "MD5"; } } else if (hashType.equals("sha256")) { - curapk.detail_hash = str; - curapk.detail_hashType = "SHA-256"; + curapk.hash = str; + curapk.hashType = "SHA-256"; } } else if (curel.equals("sig")) { curapk.sig = str; @@ -159,17 +160,17 @@ public class RepoXMLHandler extends DefaultHandler { } } else if (curel.equals("added")) { try { - curapk.added = str.length() == 0 ? null : DB.DATE_FORMAT + curapk.added = str.length() == 0 ? null : Utils.DATE_FORMAT .parse(str); } catch (ParseException e) { curapk.added = null; } } else if (curel.equals("permissions")) { - curapk.detail_permissions = DB.CommaSeparatedList.make(str); + curapk.permissions = Utils.CommaSeparatedList.make(str); } else if (curel.equals("features")) { - curapk.features = DB.CommaSeparatedList.make(str); + curapk.features = Utils.CommaSeparatedList.make(str); } else if (curel.equals("nativecode")) { - curapk.nativecode = DB.CommaSeparatedList.make(str); + curapk.nativecode = Utils.CommaSeparatedList.make(str); } } else if (curapp != null && str != null) { if (curel.equals("name")) { @@ -180,33 +181,33 @@ public class RepoXMLHandler extends DefaultHandler { // This is the old-style description. We'll read it // if present, to support old repos, but in newer // repos it will get overwritten straight away! - curapp.detail_description = "

" + str + "

"; + curapp.description = "

" + str + "

"; } else if (curel.equals("desc")) { // New-style description. - curapp.detail_description = str; + curapp.description = str; } else if (curel.equals("summary")) { curapp.summary = str; } else if (curel.equals("license")) { curapp.license = str; } else if (curel.equals("source")) { - curapp.detail_sourceURL = str; + curapp.sourceURL = str; } else if (curel.equals("donate")) { - curapp.detail_donateURL = str; + curapp.donateURL = str; } else if (curel.equals("bitcoin")) { - curapp.detail_bitcoinAddr = str; + curapp.bitcoinAddr = str; } else if (curel.equals("litecoin")) { - curapp.detail_litecoinAddr = str; + curapp.litecoinAddr = str; } else if (curel.equals("dogecoin")) { - curapp.detail_dogecoinAddr = str; + curapp.dogecoinAddr = str; } else if (curel.equals("flattr")) { - curapp.detail_flattrID = str; + curapp.flattrID = str; } else if (curel.equals("web")) { - curapp.detail_webURL = str; + curapp.webURL = str; } else if (curel.equals("tracker")) { - curapp.detail_trackerURL = str; + curapp.trackerURL = str; } else if (curel.equals("added")) { try { - curapp.added = str.length() == 0 ? null : DB.DATE_FORMAT + curapp.added = str.length() == 0 ? null : Utils.DATE_FORMAT .parse(str); } catch (ParseException e) { curapp.added = null; @@ -214,7 +215,7 @@ public class RepoXMLHandler extends DefaultHandler { } else if (curel.equals("lastupdated")) { try { curapp.lastUpdated = str.length() == 0 ? null - : DB.DATE_FORMAT.parse(str); + : Utils.DATE_FORMAT.parse(str); } catch (ParseException e) { curapp.lastUpdated = null; } @@ -227,11 +228,11 @@ public class RepoXMLHandler extends DefaultHandler { curapp.curVercode = -1; } } else if (curel.equals("categories")) { - curapp.categories = DB.CommaSeparatedList.make(str); + curapp.categories = Utils.CommaSeparatedList.make(str); } else if (curel.equals("antifeatures")) { - curapp.antiFeatures = DB.CommaSeparatedList.make(str); + curapp.antiFeatures = Utils.CommaSeparatedList.make(str); } else if (curel.equals("requirements")) { - curapp.requirements = DB.CommaSeparatedList.make(str); + curapp.requirements = Utils.CommaSeparatedList.make(str); } } else if (curel.equals("description")) { description = str; @@ -270,8 +271,7 @@ public class RepoXMLHandler extends DefaultHandler { description = dc; } else if (localName.equals("application") && curapp == null) { - curapp = new DB.App(); - curapp.detail_Populated = true; + curapp = new App(); curapp.id = attributes.getValue("", "id"); Bundle progressData = RepoUpdater.createProgressData(repo.address); progressCounter ++; diff --git a/src/org/fdroid/fdroid/SearchResults.java b/src/org/fdroid/fdroid/SearchResults.java index a9ea1d02b..426e3f76d 100644 --- a/src/org/fdroid/fdroid/SearchResults.java +++ b/src/org/fdroid/fdroid/SearchResults.java @@ -18,12 +18,10 @@ package org.fdroid.fdroid; -import java.util.ArrayList; -import java.util.List; - import android.app.ListActivity; import android.app.SearchManager; import android.content.Intent; +import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.util.Log; @@ -37,8 +35,11 @@ import android.support.v4.app.NavUtils; import android.support.v4.view.MenuItemCompat; import org.fdroid.fdroid.compat.ActionBarCompat; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.views.AppListAdapter; import org.fdroid.fdroid.views.AvailableAppListAdapter; +import org.fdroid.fdroid.views.fragments.AppListFragment; public class SearchResults extends ListActivity { @@ -46,7 +47,7 @@ public class SearchResults extends ListActivity { private static final int SEARCH = Menu.FIRST; - private AppListAdapter applist; + private AppListAdapter adapter; protected String getQuery() { Intent intent = getIntent(); @@ -73,7 +74,6 @@ public class SearchResults extends ListActivity { super.onCreate(savedInstanceState); ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); - applist = new AvailableAppListAdapter(this); setContentView(R.layout.searchresults); // Start a search by just typing @@ -102,53 +102,32 @@ public class SearchResults extends ListActivity { if (query == null || query.length() == 0) finish(); - List matchingids = new ArrayList(); - try { - DB db = DB.getDB(); - matchingids = db.doSearch(query.trim()); - } catch (Exception ex) { - Log.d("FDroid", "Search failed - " + ex.getMessage()); - } finally { - DB.releaseDB(); - } - - List apps = new ArrayList(); - List allApps = ((FDroidApp) getApplication()).getApps(); - for (DB.App app : allApps) { - for (String id : matchingids) { - if (id.equals(app.id)) { - apps.add(app); - break; - } - } - } + Cursor cursor = getContentResolver().query( + AppProvider.getSearchUri(query), AppListFragment.APP_PROJECTION, + null, null, AppListFragment.APP_SORT); TextView tv = (TextView) findViewById(R.id.description); String headertext; - if (apps.size() == 0) { + int count = cursor != null ? cursor.getCount() : 0; + if (count == 0) { headertext = getString(R.string.searchres_noapps, query); - } else if (apps.size() == 1) { + } else if (count == 1) { headertext = getString(R.string.searchres_oneapp, query); } else { - headertext = getString(R.string.searchres_napps, apps.size(), query); + headertext = getString(R.string.searchres_napps, count, query); } tv.setText(headertext); - Log.d("FDroid", "Search for '" + query + "' returned " + apps.size() - + " results"); - applist.clear(); - for (DB.App app : apps) { - applist.addItem(app); - } - getListView().setFastScrollEnabled(true); - applist.notifyDataSetChanged(); - setListAdapter(applist); + Log.d("FDroid", "Search for '" + query + "' returned " + count + " results"); + adapter = new AvailableAppListAdapter(this, cursor); + getListView().setFastScrollEnabled(true); + setListAdapter(adapter); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { - final DB.App app; - app = (DB.App) applist.getItem(position); + final App app; + app = new App((Cursor) adapter.getItem(position)); Intent intent = new Intent(this, AppDetails.class); intent.putExtra("appid", app.id); diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index 7d4063a88..4741aa51a 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -18,42 +18,26 @@ package org.fdroid.fdroid; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; +import java.util.*; -import android.app.AlarmManager; -import android.app.IntentService; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; +import android.app.*; +import android.content.*; import android.content.SharedPreferences.Editor; +import android.database.Cursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.Parcelable; -import android.os.ResultReceiver; -import android.os.SystemClock; +import android.net.Uri; +import android.os.*; import android.preference.PreferenceManager; +import android.text.TextUtils; import android.util.Log; +import org.fdroid.fdroid.data.*; +import org.fdroid.fdroid.updater.RepoUpdater; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; -import android.text.TextUtils; -import android.util.Log; import android.widget.Toast; -import org.fdroid.fdroid.data.Apk; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; -import org.fdroid.fdroid.updater.RepoUpdater; - public class UpdateService extends IntentService implements ProgressListener { public static final String RESULT_MESSAGE = "msg"; @@ -203,16 +187,6 @@ public class UpdateService extends IntentService implements ProgressListener { return receiver == null; } - // Get the number of apps that have updates available. - public int getNumUpdates(List apps) { - int count = 0; - for (DB.App app : apps) { - if (app.toUpdate) - count++; - } - return count; - } - @Override protected void onHandleIntent(Intent intent) { @@ -256,20 +230,113 @@ public class UpdateService extends IntentService implements ProgressListener { } else { Log.d("FDroid", "Unscheduled (manually requested) update"); } - errmsg = updateRepos(address); - if (TextUtils.isEmpty(errmsg)) { + + // Grab some preliminary information, then we can release the + // database while we do all the downloading, etc... + int updates = 0; + List repos = RepoProvider.Helper.all(getContentResolver()); + + // Process each repo... + Map appsToUpdate = new HashMap(); + List apksToUpdate = new ArrayList(); + List unchangedRepos = new ArrayList(); + List updatedRepos = new ArrayList(); + List disabledRepos = new ArrayList(); + boolean success = true; + boolean changes = false; + for (Repo repo : repos) { + + if (!repo.inuse) { + disabledRepos.add(repo); + continue; + } else if (!TextUtils.isEmpty(address) && !repo.address.equals(address)) { + unchangedRepos.add(repo); + continue; + } + + sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address)); + RepoUpdater updater = RepoUpdater.createUpdaterFor(getBaseContext(), repo); + updater.setProgressListener(this); + try { + updater.update(); + if (updater.hasChanged()) { + for (App app : updater.getApps()) { + appsToUpdate.put(app.id, app); + } + apksToUpdate.addAll(updater.getApks()); + updatedRepos.add(repo); + changes = true; + } else { + unchangedRepos.add(repo); + } + } catch (RepoUpdater.UpdateException e) { + errmsg += (errmsg.length() == 0 ? "" : "\n") + e.getMessage(); + Log.e("FDroid", "Error updating repository " + repo.address + ": " + e.getMessage()); + Log.e("FDroid", Log.getStackTraceString(e)); + } + } + + if (!changes && success) { + Log.d("FDroid", + "Not checking app details or compatibility, " + + "because all repos were up to date."); + } else if (changes && success) { + + sendStatus(STATUS_INFO, + getString(R.string.status_checking_compatibility)); + + List listOfAppsToUpdate = new ArrayList(); + listOfAppsToUpdate.addAll(appsToUpdate.values()); + + calcCompatibilityFlags(this, apksToUpdate, appsToUpdate); + calcIconUrls(this, apksToUpdate, appsToUpdate, repos); + calcCurrentApk(apksToUpdate, appsToUpdate); + + int totalInsertsUpdates = listOfAppsToUpdate.size() + apksToUpdate.size(); + updateOrInsertApps(listOfAppsToUpdate, totalInsertsUpdates, 0); + updateOrInsertApks(apksToUpdate, totalInsertsUpdates, listOfAppsToUpdate.size()); + removeApksFromRepos(disabledRepos); + removeApksNoLongerInRepo(listOfAppsToUpdate, updatedRepos); + removeAppsWithoutApks(); + notifyContentProviders(); + } + + if (success && changes && prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, false)) { + int updateCount = 0; + for (App app : appsToUpdate.values()) { + if (app.hasUpdates(this)) { + updateCount ++; + } + } + + if (updateCount > 0) { + showAppUpdatesNotification(updateCount); + } + } + + if (!success) { + if (errmsg.length() == 0) + errmsg = "Unknown error"; + sendStatus(STATUS_ERROR, errmsg); + } else { Editor e = prefs.edit(); e.putLong(Preferences.PREF_UPD_LAST, System.currentTimeMillis()); e.commit(); + if (changes) { + sendStatus(STATUS_COMPLETE_WITH_CHANGES); + } else { + sendStatus(STATUS_COMPLETE_AND_SAME); + } } + } catch (Exception e) { Log.e("FDroid", "Exception during update processing:\n" + Log.getStackTraceString(e)); - if (TextUtils.isEmpty(errmsg)) + if (errmsg.length() == 0) errmsg = "Unknown error"; sendStatus(STATUS_ERROR, errmsg); - } finally { + } finally { Log.d("FDroid", "Update took " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds."); @@ -277,147 +344,112 @@ public class UpdateService extends IntentService implements ProgressListener { } } - protected String updateRepos(String address) throws Exception { - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(getBaseContext()); - boolean notify = prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, false); - String errmsg = ""; - // Grab some preliminary information, then we can release the - // database while we do all the downloading, etc... - int updates = 0; - List repos; - List apps; - try { - DB db = DB.getDB(); - apps = db.getApps(false); - } finally { - DB.releaseDB(); - } + private void notifyContentProviders() { + getContentResolver().notifyChange(AppProvider.getContentUri(), null); + getContentResolver().notifyChange(ApkProvider.getContentUri(), null); + } - repos = RepoProvider.Helper.all(getContentResolver()); - - // Process each repo... - List updatingApps = new ArrayList(); - Set keeprepos = new TreeSet(); - boolean changes = false; - boolean update; - for (Repo repo : repos) { - if (!repo.inuse) - continue; - // are we updating all repos, or just one? - if (TextUtils.isEmpty(address)) { - update = true; + private static void calcCompatibilityFlags(Context context, List apks, + Map apps) { + CompatibilityChecker checker = new CompatibilityChecker(context); + for (Apk apk : apks) { + List reasons = checker.getIncompatibleReasons(apk); + if (reasons.size() > 0) { + apk.compatible = false; + apk.incompatible_reasons = Utils.CommaSeparatedList.make(reasons); } else { - // if only updating one repo, mark the rest as keepers - if (address.equals(repo.address)) { - update = true; + apk.compatible = true; + apk.incompatible_reasons = null; + apps.get(apk.id).compatible = true; + } + } + } + + /** + * Get the current version - this will be one of the Apks from 'apks'. + * Can return null if there are no available versions. + * This should be the 'current' version, as in the most recent stable + * one, that most users would want by default. It might not be the + * most recent, if for example there are betas etc. + */ + private static void calcCurrentApk(List apks, Map apps ) { + for ( App app : apps.values() ) { + List apksForApp = new ArrayList(); + for (Apk apk : apks) { + if (apk.id.equals(app.id)) { + apksForApp.add(apk); + } + } + calcCurrentApkForApp(app, apksForApp); + } + } + + private static void calcCurrentApkForApp(App app, List apksForApp) { + Apk latestApk = null; + // Try and return the real current version first. It will find the + // closest version smaller than the curVercode, being the same + // vercode if it exists. + if (app.curVercode > 0) { + int latestcode = -1; + for (Apk apk : apksForApp) { + if ((!app.compatible || apk.compatible) + && apk.vercode <= app.curVercode + && apk.vercode > latestcode) { + latestApk = apk; + latestcode = apk.vercode; + } + } + } else if (app.curVercode == -1) { + // If the current version was not set we return the most recent apk. + int latestCode = -1; + for (Apk apk : apksForApp) { + if ((!app.compatible || apk.compatible) + && apk.vercode > latestCode) { + latestApk = apk; + latestCode = apk.vercode; + } + } + } + + if (latestApk != null) { + app.curVercode = latestApk.vercode; + app.curVersion = latestApk.version; + } + } + + private static void calcIconUrls(Context context, List apks, + Map apps, List repos) { + String iconsDir = Utils.getIconsDir(context); + Log.d("FDroid", "Density-specific icons dir is " + iconsDir); + for (App app : apps.values()) { + if (app.iconUrl == null && app.icon != null) { + calcIconUrl(iconsDir, app, apks, repos); + } + } + } + + private static void calcIconUrl(String iconsDir, App app, + List allApks, List repos) { + List apksForApp = new ArrayList(); + for (Apk apk : allApks) { + if (apk.id.equals(app.id)) { + apksForApp.add(apk); + } + } + + Collections.sort(apksForApp); + for (int i = apksForApp.size() - 1; i >= 0; i --) { + Apk apk = apksForApp.get(i); + for (Repo repo : repos) { + if (repo.getId() != apk.repo) continue; + if (repo.version >= Repo.VERSION_DENSITY_SPECIFIC_ICONS) { + app.iconUrl = repo.address + iconsDir + app.icon; } else { - keeprepos.add(repo.getId()); - update = false; + app.iconUrl = repo.address + "/icons/" + app.icon; } - } - if (!update) - continue; - sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address)); - RepoUpdater updater = RepoUpdater.createUpdaterFor(getBaseContext(), repo); - updater.setProgressListener(this); - try { - updater.update(); - if (updater.hasChanged()) { - updatingApps.addAll(updater.getApps()); - changes = true; - } else { - keeprepos.add(repo.getId()); - } - } catch (RepoUpdater.UpdateException e) { - errmsg += (errmsg.length() == 0 ? "" : "\n") + e.getMessage(); - Log.e("FDroid", "Error updating repository " + repo.address + ": " + e.getMessage()); - Log.e("FDroid", Log.getStackTraceString(e)); + return; } } - - boolean success = true; - if (!changes) { - Log.d("FDroid", "Not checking app details or compatibility, " + - "because all repos were up to date."); - } else { - sendStatus(STATUS_INFO, getString(R.string.status_checking_compatibility)); - - DB db = DB.getDB(); - try { - - // Need to flag things we're keeping despite having received - // no data about during the update. (i.e. stuff from a repo - // that we know is unchanged due to the etag) - for (long keep : keeprepos) { - for (DB.App app : apps) { - boolean keepapp = false; - for (Apk apk : app.apks) { - if (apk.repo == keep) { - keepapp = true; - break; - } - } - if (keepapp) { - DB.App app_k = null; - for (DB.App app2 : apps) { - if (app2.id.equals(app.id)) { - app_k = app2; - break; - } - } - if (app_k == null) { - updatingApps.add(app); - app_k = app; - } - app_k.updated = true; - db.populateDetails(app_k, keep); - for (Apk apk : app.apks) - if (apk.repo == keep) - apk.updated = true; - } - } - } - - db.beginUpdate(apps); - for (DB.App app : updatingApps) { - db.updateApplication(app); - } - db.endUpdate(); - } catch (Exception ex) { - db.cancelUpdate(); - Log.e("FDroid", "Exception during update processing:\n" - + Log.getStackTraceString(ex)); - errmsg = "Exception during processing - " + ex.getMessage(); - success = false; - } finally { - DB.releaseDB(); - } - } - - if (success && changes) { - ((FDroidApp) getApplication()).invalidateAllApps(); - if (notify) { - apps = ((FDroidApp) getApplication()).getApps(); - updates = getNumUpdates(apps); - } - if (notify && updates > 0) - showAppUpdatesNotification(updates); - } - - if (success) { - if (changes) { - sendStatus(STATUS_COMPLETE_WITH_CHANGES); - } else { - sendStatus(STATUS_COMPLETE_AND_SAME); - } - } else { - if (TextUtils.isEmpty(errmsg)) - errmsg = "Unknown error"; - sendStatus(STATUS_ERROR, errmsg); - } - - return errmsg; } private void showAppUpdatesNotification(int updates) throws Exception { @@ -447,6 +479,209 @@ public class UpdateService extends IntentService implements ProgressListener { nm.notify(1, builder.build()); } + private List getKnownAppIds(List apps) { + List knownAppIds = new ArrayList(); + if (apps.size() > AppProvider.MAX_APPS_TO_QUERY) { + int middle = apps.size() / 2; + List apps1 = apps.subList(0, middle); + List apps2 = apps.subList(middle, apps.size()); + knownAppIds.addAll(getKnownAppIds(apps1)); + knownAppIds.addAll(getKnownAppIds(apps2)); + } else { + knownAppIds.addAll(getKnownAppIdsFromProvider(apps)); + } + return knownAppIds; + } + + /** + * Looks in the database to see which apps we already know about. Only + * returns ids of apps that are in the database if they are in the "apps" + * array. + */ + private List getKnownAppIdsFromProvider(List apps) { + + Uri uri = AppProvider.getContentUri(apps); + String[] fields = new String[] { AppProvider.DataColumns.APP_ID }; + Cursor cursor = getContentResolver().query(uri, fields, null, null, null); + + int knownIdCount = cursor != null ? cursor.getCount() : 0; + List knownIds = new ArrayList(knownIdCount); + if (knownIdCount > 0) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + knownIds.add(cursor.getString(0)); + cursor.moveToNext(); + } + } + + return knownIds; + } + + /** + * If you call this with too many apks, then it will likely hit limit of + * parameters allowed for sqlite3 query. Rather, you should use + * {@link org.fdroid.fdroid.UpdateService#getKnownApks(java.util.List)} + * instead, which will only call this with the right number of apks at + * a time. + * @see org.fdroid.fdroid.UpdateService#getKnownAppIds(java.util.List) + */ + private List getKnownApksFromProvider(List apks) { + String[] fields = { + ApkProvider.DataColumns.APK_ID, + ApkProvider.DataColumns.VERSION, + ApkProvider.DataColumns.VERSION_CODE + }; + return ApkProvider.Helper.knownApks(getContentResolver(), apks, fields); + } + + private void updateOrInsertApps(List appsToUpdate, int totalUpdateCount, int currentCount) { + + ArrayList operations = new ArrayList(); + List knownAppIds = getKnownAppIds(appsToUpdate); + for (App a : appsToUpdate) { + boolean known = false; + for (String knownId : knownAppIds) { + if (knownId.equals(a.id)) { + known = true; + break; + } + } + + if (known) { + operations.add(updateExistingApp(a)); + } else { + operations.add(insertNewApp(a)); + } + } + + Log.d("FDroid", "Updating/inserting " + operations.size() + " apps."); + try { + executeBatchWithStatus(AppProvider.getAuthority(), operations, currentCount, totalUpdateCount); + } catch (RemoteException e) { + Log.e("FDroid", e.getMessage()); + } catch (OperationApplicationException e) { + Log.e("FDroid", e.getMessage()); + } + } + + private void executeBatchWithStatus(String providerAuthority, + ArrayList operations, + int currentCount, + int totalUpdateCount) + throws RemoteException, OperationApplicationException { + int i = 0; + while (i < operations.size()) { + int count = Math.min(operations.size() - i, 100); + ArrayList o = new ArrayList(operations.subList(i, i + count)); + sendStatus(STATUS_INFO, getString( + R.string.status_inserting, + (int)((double)(currentCount + i) / totalUpdateCount * 100))); + getContentResolver().applyBatch(providerAuthority, o); + i += 100; + } + } + + /** + * Return list of apps from "fromApks" which are already in the database. + */ + private List getKnownApks(List apks) { + List knownApks = new ArrayList(); + if (apks.size() > ApkProvider.MAX_APKS_TO_QUERY) { + int middle = apks.size() / 2; + List apks1 = apks.subList(0, middle); + List apks2 = apks.subList(middle, apks.size()); + knownApks.addAll(getKnownApks(apks1)); + knownApks.addAll(getKnownApks(apks2)); + } else { + knownApks.addAll(getKnownApksFromProvider(apks)); + } + return knownApks; + } + + private void updateOrInsertApks(List apksToUpdate, int totalApksAppsCount, int currentCount) { + + ArrayList operations = new ArrayList(); + + List knownApks = getKnownApks(apksToUpdate); + for (Apk apk : apksToUpdate) { + boolean known = false; + for (Apk knownApk : knownApks) { + if (knownApk.id.equals(apk.id) && knownApk.version.equals(knownApk.version)) { + known = true; + break; + } + } + + if (known) { + operations.add(updateExistingApk(apk)); + } else { + operations.add(insertNewApk(apk)); + } + } + + Log.d("FDroid", "Updating/inserting " + operations.size() + " apks."); + try { + executeBatchWithStatus(ApkProvider.getAuthority(), operations, currentCount, totalApksAppsCount); + } catch (RemoteException e) { + Log.e("FDroid", e.getMessage()); + } catch (OperationApplicationException e) { + Log.e("FDroid", e.getMessage()); + } + } + + private ContentProviderOperation updateExistingApk(Apk apk) { + Uri uri = ApkProvider.getContentUri(apk); + ContentValues values = apk.toContentValues(); + return ContentProviderOperation.newUpdate(uri).withValues(values).build(); + } + + private ContentProviderOperation insertNewApk(Apk apk) { + ContentValues values = apk.toContentValues(); + Uri uri = ApkProvider.getContentUri(); + return ContentProviderOperation.newInsert(uri).withValues(values).build(); + } + + private ContentProviderOperation updateExistingApp(App app) { + Uri uri = AppProvider.getContentUri(app); + ContentValues values = app.toContentValues(); + return ContentProviderOperation.newUpdate(uri).withValues(values).build(); + } + + private ContentProviderOperation insertNewApp(App app) { + ContentValues values = app.toContentValues(); + Uri uri = AppProvider.getContentUri(); + return ContentProviderOperation.newInsert(uri).withValues(values).build(); + } + + /** + * If a repo was updated (i.e. it is in use, and the index has changed + * since last time we did an update), then we want to remove any apks that + * belong to the repo which are not in the current list of apks that were + * retrieved. + */ + private void removeApksNoLongerInRepo(List appsToUpdate, + List updatedRepos) { + for (Repo repo : updatedRepos) { + Log.d("FDroid", "Removing apks no longer in repo " + repo.address); + // TODO: Implement + } + + } + + private void removeApksFromRepos(List repos) { + for (Repo repo : repos) { + Log.d("FDroid", "Removing apks from repo " + repo.address); + Uri uri = ApkProvider.getRepoUri(repo.getId()); + getContentResolver().delete(uri, null, null); + } + } + + private void removeAppsWithoutApks() { + Log.d("FDroid", "Removing aps that don't have any apks"); + getContentResolver().delete(AppProvider.getNoApksUri(), null, null); + } + + /** * Received progress event from the RepoXMLHandler. It could be progress * downloading from the repo, or perhaps processing the info from the repo. diff --git a/src/org/fdroid/fdroid/Utils.java b/src/org/fdroid/fdroid/Utils.java index 767b30048..97f4e4a92 100644 --- a/src/org/fdroid/fdroid/Utils.java +++ b/src/org/fdroid/fdroid/Utils.java @@ -20,22 +20,60 @@ package org.fdroid.fdroid; import android.content.Context; +import android.content.pm.PackageInfo; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; import com.nostra13.universalimageloader.utils.StorageUtils; -import java.io.*; +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.File; +import java.io.FileReader; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; import java.text.SimpleDateFormat; -import java.util.Locale; +import java.security.MessageDigest; +import java.util.*; + +import org.fdroid.fdroid.data.Repo; public final class Utils { public static final int BUFFER_SIZE = 4096; + // The date format used for storing dates (e.g. lastupdated, added) in the + // database. + public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); + private static final String[] FRIENDLY_SIZE_FORMAT = { "%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB" }; public static final SimpleDateFormat LOG_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH); + public static String getIconsDir(Context context) { + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + String iconsDir; + if (metrics.densityDpi >= 640) { + iconsDir = "/icons-640/"; + } else if (metrics.densityDpi >= 480) { + iconsDir = "/icons-480/"; + } else if (metrics.densityDpi >= 320) { + iconsDir = "/icons-320/"; + } else if (metrics.densityDpi >= 240) { + iconsDir = "/icons-240/"; + } else if (metrics.densityDpi >= 160) { + iconsDir = "/icons-160/"; + } else { + iconsDir = "/icons-120/"; + } + return iconsDir; + } + public static void copy(InputStream input, OutputStream output) throws IOException { copy(input, output, null, null); @@ -158,4 +196,145 @@ public final class Utils { return apkCacheDir; } + public static Map getInstalledApps(Context context) { + return installedApkCache.getApks(context); + } + + public static void clearInstalledApksCache() { + installedApkCache.emptyCache(); + } + + public static String calcFingerprint(String keyHexString) { + if (TextUtils.isEmpty(keyHexString)) + return null; + else + return calcFingerprint(Hasher.unhex(keyHexString)); + } + + public static String calcFingerprint(Certificate cert) { + try { + return calcFingerprint(cert.getEncoded()); + } catch (CertificateEncodingException e) { + return null; + } + } + + public static String calcFingerprint(byte[] key) { + String ret = null; + try { + // keytool -list -v gives you the SHA-256 fingerprint + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update(key); + byte[] fingerprint = digest.digest(); + Formatter formatter = new Formatter(new StringBuilder()); + for (int i = 1; i < fingerprint.length; i++) { + formatter.format("%02X", fingerprint[i]); + } + ret = formatter.toString(); + formatter.close(); + } catch (Exception e) { + Log.w("FDroid", "Unable to get certificate fingerprint.\n" + + Log.getStackTraceString(e)); + } + return ret; + } + + public static class CommaSeparatedList implements Iterable { + private String value; + + private CommaSeparatedList(String list) { + value = list; + } + + public static CommaSeparatedList make(List list) { + if (list == null || list.size() == 0) + return null; + else { + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < list.size(); i ++) { + if (i > 0) { + sb.append(','); + } + sb.append(list.get(i)); + } + return new CommaSeparatedList(sb.toString()); + } + } + + public static CommaSeparatedList make(String list) { + if (list == null || list.length() == 0) + return null; + else + return new CommaSeparatedList(list); + } + + public static String str(CommaSeparatedList instance) { + return (instance == null ? null : instance.toString()); + } + + @Override + public String toString() { + return value; + } + + public String toPrettyString() { + return value.replaceAll(",", ", "); + } + + @Override + public Iterator iterator() { + TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(','); + splitter.setString(value); + return splitter.iterator(); + } + + public boolean contains(String v) { + for (String s : this) { + if (s.equals(v)) + return true; + } + return false; + } + } + + private static InstalledApkCache installedApkCache = null; + + /** + * We do a lot of querying of the installed app's. As a result, we like + * to cache this information quite heavily (and flush the cache when new + * apps are installed). The caching implementation needs to be setup like + * this so that it is possible to mock for testing purposes. + */ + public static void setupInstalledApkCache(InstalledApkCache cache) { + installedApkCache = cache; + } + + public static class InstalledApkCache { + + protected Map installedApks = null; + + protected Map buildAppList(Context context) { + Map info = new HashMap(); + Log.d("FDroid", "Reading installed packages"); + List installedPackages = context.getPackageManager().getInstalledPackages(0); + for (PackageInfo appInfo : installedPackages) { + info.put(appInfo.packageName, appInfo); + } + return info; + } + + public Map getApks(Context context) { + if (installedApks == null) { + installedApks = buildAppList(context); + } + return installedApks; + } + + public void emptyCache() { + installedApks = null; + } + + } + + } diff --git a/src/org/fdroid/fdroid/data/Apk.java b/src/org/fdroid/fdroid/data/Apk.java index cf0c0a0f7..9f6b305f1 100644 --- a/src/org/fdroid/fdroid/data/Apk.java +++ b/src/org/fdroid/fdroid/data/Apk.java @@ -1,37 +1,27 @@ package org.fdroid.fdroid.data; import android.content.ContentValues; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.pm.FeatureInfo; -import android.content.pm.PackageManager; import android.database.Cursor; -import android.preference.PreferenceManager; -import android.util.Log; -import org.fdroid.fdroid.DB; -import org.fdroid.fdroid.compat.Compatibility; -import org.fdroid.fdroid.compat.SupportedArchitectures; +import org.fdroid.fdroid.Utils; -import java.util.Date; -import java.util.Set; -import java.util.HashSet; +import java.util.*; -public class Apk { +public class Apk extends ValueObject implements Comparable { public String id; public String version; public int vercode; - public int detail_size; // Size in bytes - 0 means we don't know! + public int size; // Size in bytes - 0 means we don't know! public long repo; // ID of the repo it comes from - public String detail_hash; - public String detail_hashType; + public String hash; + public String hashType; public int minSdkVersion; // 0 if unknown public Date added; - public DB.CommaSeparatedList detail_permissions; // null if empty or + public Utils.CommaSeparatedList permissions; // null if empty or // unknown - public DB.CommaSeparatedList features; // null if empty or unknown + public Utils.CommaSeparatedList features; // null if empty or unknown - public DB.CommaSeparatedList nativecode; // null if empty or unknown + public Utils.CommaSeparatedList nativecode; // null if empty or unknown // ID (md5 sum of public key) of signature. Might be null, in the // transition to this field existing. @@ -52,30 +42,33 @@ public class Apk { public int repoVersion; public String repoAddress; - public DB.CommaSeparatedList incompatible_reasons; + public Utils.CommaSeparatedList incompatible_reasons; public Apk() { updated = false; - detail_size = 0; + size = 0; added = null; repo = 0; - detail_hash = null; - detail_hashType = null; - detail_permissions = null; + hash = null; + hashType = null; + permissions = null; compatible = false; } public Apk(Cursor cursor) { + + checkCursorPosition(cursor); + for(int i = 0; i < cursor.getColumnCount(); i ++ ) { String column = cursor.getColumnName(i); if (column.equals(ApkProvider.DataColumns.HASH)) { - detail_hash = cursor.getString(i); + hash = cursor.getString(i); } else if (column.equals(ApkProvider.DataColumns.HASH_TYPE)) { - detail_hashType = cursor.getString(i); + hashType = cursor.getString(i); } else if (column.equals(ApkProvider.DataColumns.ADDED_DATE)) { added = ValueObject.toDate(cursor.getString(i)); } else if (column.equals(ApkProvider.DataColumns.FEATURES)) { - features = DB.CommaSeparatedList.make(cursor.getString(i)); + features = Utils.CommaSeparatedList.make(cursor.getString(i)); } else if (column.equals(ApkProvider.DataColumns.APK_ID)) { id = cursor.getString(i); } else if (column.equals(ApkProvider.DataColumns.IS_COMPATIBLE)) { @@ -85,15 +78,17 @@ public class Apk { } else if (column.equals(ApkProvider.DataColumns.NAME)) { apkName = cursor.getString(i); } else if (column.equals(ApkProvider.DataColumns.PERMISSIONS)) { - detail_permissions = DB.CommaSeparatedList.make(cursor.getString(i)); + permissions = Utils.CommaSeparatedList.make(cursor.getString(i)); } else if (column.equals(ApkProvider.DataColumns.NATIVE_CODE)) { - nativecode = DB.CommaSeparatedList.make(cursor.getString(i)); + nativecode = Utils.CommaSeparatedList.make(cursor.getString(i)); + } else if (column.equals(ApkProvider.DataColumns.INCOMPATIBLE_REASONS)) { + incompatible_reasons = Utils.CommaSeparatedList.make(cursor.getString(i)); } else if (column.equals(ApkProvider.DataColumns.REPO_ID)) { repo = cursor.getInt(i); } else if (column.equals(ApkProvider.DataColumns.SIGNATURE)) { sig = cursor.getString(i); } else if (column.equals(ApkProvider.DataColumns.SIZE)) { - detail_size = cursor.getInt(i); + size = cursor.getInt(i); } else if (column.equals(ApkProvider.DataColumns.SOURCE_NAME)) { srcname = cursor.getString(i); } else if (column.equals(ApkProvider.DataColumns.VERSION)) { @@ -108,109 +103,36 @@ public class Apk { } } + @Override + public String toString() { + return id + " (version " + vercode + ")"; + } + public ContentValues toContentValues() { ContentValues values = new ContentValues(); values.put(ApkProvider.DataColumns.APK_ID, id); values.put(ApkProvider.DataColumns.VERSION, version); values.put(ApkProvider.DataColumns.VERSION_CODE, vercode); values.put(ApkProvider.DataColumns.REPO_ID, repo); - values.put(ApkProvider.DataColumns.HASH, detail_hash); - values.put(ApkProvider.DataColumns.HASH_TYPE, detail_hashType); + values.put(ApkProvider.DataColumns.HASH, hash); + values.put(ApkProvider.DataColumns.HASH_TYPE, hashType); values.put(ApkProvider.DataColumns.SIGNATURE, sig); values.put(ApkProvider.DataColumns.SOURCE_NAME, srcname); - values.put(ApkProvider.DataColumns.SIZE, detail_size); + values.put(ApkProvider.DataColumns.SIZE, size); values.put(ApkProvider.DataColumns.NAME, apkName); values.put(ApkProvider.DataColumns.MIN_SDK_VERSION, minSdkVersion); - values.put(ApkProvider.DataColumns.ADDED_DATE, - added == null ? "" : DB.DATE_FORMAT.format(added)); - values.put(ApkProvider.DataColumns.PERMISSIONS, - DB.CommaSeparatedList.str(detail_permissions)); - values.put(ApkProvider.DataColumns.FEATURES, DB.CommaSeparatedList.str(features)); - values.put(ApkProvider.DataColumns.NATIVE_CODE, DB.CommaSeparatedList.str(nativecode)); + values.put(ApkProvider.DataColumns.ADDED_DATE, added == null ? "" : Utils.DATE_FORMAT.format(added)); + values.put(ApkProvider.DataColumns.PERMISSIONS, Utils.CommaSeparatedList.str(permissions)); + values.put(ApkProvider.DataColumns.FEATURES, Utils.CommaSeparatedList.str(features)); + values.put(ApkProvider.DataColumns.NATIVE_CODE, Utils.CommaSeparatedList.str(nativecode)); + values.put(ApkProvider.DataColumns.INCOMPATIBLE_REASONS, Utils.CommaSeparatedList.str(incompatible_reasons)); values.put(ApkProvider.DataColumns.IS_COMPATIBLE, compatible ? 1 : 0); return values; } - // Call isCompatible(apk) on an instance of this class to - // check if an APK is compatible with the user's device. - public static class CompatibilityChecker extends Compatibility { - - private Set features; - private Set cpuAbis; - private String cpuAbisDesc; - private boolean ignoreTouchscreen; - - public CompatibilityChecker(Context ctx) { - - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(ctx); - ignoreTouchscreen = prefs - .getBoolean("ignoreTouchscreen", false); - - PackageManager pm = ctx.getPackageManager(); - StringBuilder logMsg = new StringBuilder(); - logMsg.append("Available device features:"); - features = new HashSet(); - if (pm != null) { - for (FeatureInfo fi : pm.getSystemAvailableFeatures()) { - features.add(fi.name); - logMsg.append('\n'); - logMsg.append(fi.name); - } - } - - cpuAbis = SupportedArchitectures.getAbis(); - - StringBuilder builder = new StringBuilder(); - boolean first = true; - for (String abi : cpuAbis) { - if (first) first = false; - else builder.append(", "); - builder.append(abi); - } - cpuAbisDesc = builder.toString(); - builder = null; - - Log.d("FDroid", logMsg.toString()); - } - - private boolean compatibleApi(DB.CommaSeparatedList nativecode) { - if (nativecode == null) return true; - for (String abi : nativecode) { - if (cpuAbis.contains(abi)) { - return true; - } - } - return false; - } - - public boolean isCompatible(Apk apk) { - if (!hasApi(apk.minSdkVersion)) { - apk.incompatible_reasons = DB.CommaSeparatedList.make(String.valueOf(apk.minSdkVersion)); - return false; - } - if (apk.features != null) { - for (String feat : apk.features) { - if (ignoreTouchscreen - && feat.equals("android.hardware.touchscreen")) { - // Don't check it! - } else if (!features.contains(feat)) { - apk.incompatible_reasons = DB.CommaSeparatedList.make(feat); - Log.d("FDroid", apk.id + " vercode " + apk.vercode - + " is incompatible based on lack of " - + feat); - return false; - } - } - } - if (!compatibleApi(apk.nativecode)) { - apk.incompatible_reasons = apk.nativecode; - Log.d("FDroid", apk.id + " vercode " + apk.vercode - + " only supports " + DB.CommaSeparatedList.str(apk.nativecode) - + " while your architectures are " + cpuAbisDesc); - return false; - } - return true; - } + @Override + public int compareTo(Apk apk) { + return Integer.valueOf(vercode).compareTo(apk.vercode); } + } diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java index b1fc75f30..a4b16b39c 100644 --- a/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -8,12 +8,19 @@ import android.database.Cursor; import android.net.Uri; import android.provider.BaseColumns; import android.util.Log; -import org.fdroid.fdroid.DB; import java.util.*; public class ApkProvider extends FDroidProvider { + /** + * SQLite has a maximum of 999 parameters in a query. Each apk we add + * requires two (id and vercode) so we can only query half of that. Then, + * we may want to add additional constraints, so we give our self some + * room by saying only 450 apks can be queried at once. + */ + public static final int MAX_APKS_TO_QUERY = 450; + public static final class Helper { private Helper() {} @@ -75,10 +82,10 @@ public class ApkProvider extends FDroidProvider { Uri uri = getContentUri(); String[] args = { Long.toString(repo.getId()) }; String selection = DataColumns.REPO_ID + " = ?"; - resolver.delete(uri, selection + " = ?", args); + int count = resolver.delete(uri, selection, args); } - public static void deleteApksByApp(Context context, DB.App app) { + public static void deleteApksByApp(Context context, App app) { ContentResolver resolver = context.getContentResolver(); Uri uri = getContentUri(); String[] args = { app.id }; @@ -95,6 +102,7 @@ public class ApkProvider extends FDroidProvider { Uri uri = getContentUri(id, versionCode); Cursor cursor = resolver.query(uri, projection, null, null, null); if (cursor != null && cursor.getCount() > 0) { + cursor.moveToFirst(); return new Apk(cursor); } else { return null; @@ -106,6 +114,29 @@ public class ApkProvider extends FDroidProvider { Uri uri = getContentUri(id, versionCode); resolver.delete(uri, null, null); } + + public static List findByApp(ContentResolver resolver, String appId) { + return findByApp(resolver, appId, ApkProvider.DataColumns.ALL); + } + + public static List findByApp(ContentResolver resolver, + String appId, String[] projection) { + Uri uri = getAppUri(appId); + String sort = ApkProvider.DataColumns.VERSION_CODE + " DESC"; + Cursor cursor = resolver.query(uri, projection, null, null, sort); + return cursorToList(cursor); + } + + /** + * Returns apks in the database, which have the same id and version as + * one of the apks in the "apks" argument. + */ + public static List knownApks(ContentResolver resolver, + List apks, String[] fields) { + Uri uri = getContentUri(apks); + Cursor cursor = resolver.query(uri, fields, null, null, null); + return cursorToList(cursor); + } } public interface DataColumns extends BaseColumns { @@ -126,6 +157,7 @@ public class ApkProvider extends FDroidProvider { public static String HASH_TYPE = "hashType"; public static String ADDED_DATE = "added"; public static String IS_COMPATIBLE = "compatible"; + public static String INCOMPATIBLE_REASONS = "incompatibleReasons"; public static String REPO_VERSION = "repoVersion"; public static String REPO_ADDRESS = "repoAddress"; @@ -133,12 +165,19 @@ public class ApkProvider extends FDroidProvider { _ID, APK_ID, VERSION, REPO_ID, HASH, VERSION_CODE, NAME, SIZE, SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, PERMISSIONS, FEATURES, NATIVE_CODE, HASH_TYPE, ADDED_DATE, IS_COMPATIBLE, - - REPO_VERSION, REPO_ADDRESS + REPO_VERSION, REPO_ADDRESS, INCOMPATIBLE_REASONS }; } + private static final int CODE_APP = CODE_SINGLE + 1; + private static final int CODE_REPO = CODE_APP + 1; + private static final int CODE_APKS = CODE_REPO + 1; + private static final String PROVIDER_NAME = "ApkProvider"; + private static final String PATH_APK = "apk"; + private static final String PATH_APKS = "apks"; + private static final String PATH_APP = "app"; + private static final String PATH_REPO = "repo"; private static final UriMatcher matcher = new UriMatcher(-1); @@ -148,22 +187,65 @@ public class ApkProvider extends FDroidProvider { REPO_FIELDS.put(DataColumns.REPO_VERSION, RepoProvider.DataColumns.VERSION); REPO_FIELDS.put(DataColumns.REPO_ADDRESS, RepoProvider.DataColumns.ADDRESS); - matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, null, CODE_LIST); - matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, "/*/#", CODE_SINGLE); + matcher.addURI(getAuthority(), PATH_REPO + "/#", CODE_REPO); + matcher.addURI(getAuthority(), PATH_APK + "/#/*", CODE_SINGLE); + matcher.addURI(getAuthority(), PATH_APKS + "/*", CODE_APKS); + matcher.addURI(getAuthority(), PATH_APP + "/*", CODE_APP); + matcher.addURI(getAuthority(), null, CODE_LIST); + } + + public static String getAuthority() { + return AUTHORITY + "." + PROVIDER_NAME; } public static Uri getContentUri() { - return Uri.parse("content://" + AUTHORITY + "." + PROVIDER_NAME); + return Uri.parse("content://" + getAuthority()); + } + + public static Uri getAppUri(String appId) { + return getContentUri() + .buildUpon() + .appendPath(PATH_APP) + .appendPath(appId) + .build(); + } + + public static Uri getRepoUri(long repoId) { + return getContentUri() + .buildUpon() + .appendPath(PATH_REPO) + .appendPath(Long.toString(repoId)) + .build(); + } + + public static Uri getContentUri(Apk apk) { + return getContentUri(apk.id, apk.vercode); } public static Uri getContentUri(String id, int versionCode) { return getContentUri() .buildUpon() + .appendPath(PATH_APK) .appendPath(Integer.toString(versionCode)) .appendPath(id) .build(); } + public static Uri getContentUri(List apks) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < apks.size(); i ++) { + if (i != 0) { + builder.append(','); + } + Apk a = apks.get(i); + builder.append(a.id).append(':').append(a.vercode); + } + return getContentUri().buildUpon() + .appendPath(PATH_APKS) + .appendPath(builder.toString()) + .build(); + } + @Override protected String getTableName() { return DBHelper.TABLE_APK; @@ -257,29 +339,70 @@ public class ApkProvider extends FDroidProvider { } } - private String appendPrimaryKeyToSelection(String selection) { - return (selection == null ? "" : selection + " AND ") + " id = ? and vercode = ?"; + private QuerySelection queryApp(String appId) { + String selection = " id = ? "; + String[] args = new String[] { appId }; + return new QuerySelection(selection, args); } - private String[] appendPrimaryKeyToArgs(Uri uri, String[] selectionArgs) { - List args = new ArrayList(selectionArgs.length + 2); - for (String arg : args) { - args.add(arg); + private QuerySelection querySingle(Uri uri) { + String selection = " vercode = ? and id = ? "; + String[] args = new String[] { + // First (0th) path segment is the word "apk", + // and we are not interested in it. + uri.getPathSegments().get(1), + uri.getPathSegments().get(2) + }; + return new QuerySelection(selection, args); + } + + private QuerySelection queryRepo(long repoId) { + String selection = " repo = ? "; + String[] args = new String[]{ Long.toString(repoId) }; + return new QuerySelection(selection, args); + } + + private QuerySelection queryApks(String apkKeys) { + String[] apkDetails = apkKeys.split(","); + String[] args = new String[apkDetails.length * 2]; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < apkDetails.length; i ++) { + String[] parts = apkDetails[i].split(":"); + String id = parts[0]; + String verCode = parts[1]; + args[i * 2] = id; + args[i * 2 + 1] = verCode; + if (i != 0) { + sb.append(" OR "); + } + sb.append(" ( id = ? AND vercode = ? ) "); } - args.addAll(uri.getPathSegments()); - return (String[])args.toArray(); + return new QuerySelection(sb.toString(), args); } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + QuerySelection query = new QuerySelection(selection, selectionArgs); + switch (matcher.match(uri)) { case CODE_LIST: break; case CODE_SINGLE: - selection = appendPrimaryKeyToSelection(selection); - selectionArgs = appendPrimaryKeyToArgs(uri, selectionArgs); + query = query.add(querySingle(uri)); + break; + + case CODE_APP: + query = query.add(queryApp(uri.getLastPathSegment())); + break; + + case CODE_APKS: + query = query.add(queryApks(uri.getLastPathSegment())); + break; + + case CODE_REPO: + query = query.add(queryRepo(Long.parseLong(uri.getLastPathSegment()))); break; default: @@ -287,14 +410,14 @@ public class ApkProvider extends FDroidProvider { throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri); } - QueryBuilder query = new QueryBuilder(); + QueryBuilder queryBuilder = new QueryBuilder(); for (String field : projection) { - query.addField(field); + queryBuilder.addField(field); } - query.addSelection(selection); - query.addOrderBy(sortOrder); + queryBuilder.addSelection(query.getSelection()); + queryBuilder.addOrderBy(sortOrder); - Cursor cursor = read().rawQuery(query.toString(), selectionArgs); + Cursor cursor = read().rawQuery(queryBuilder.toString(), query.getArgs()); cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; } @@ -313,10 +436,11 @@ public class ApkProvider extends FDroidProvider { @Override public Uri insert(Uri uri, ContentValues values) { - removeRepoFields(values); long id = write().insertOrThrow(getTableName(), null, values); - getContext().getContentResolver().notifyChange(uri, null); + if (!isApplyingBatch()) { + getContext().getContentResolver().notifyChange(uri, null); + } return getContentUri( values.getAsString(DataColumns.APK_ID), values.getAsInteger(DataColumns.VERSION_CODE)); @@ -326,14 +450,19 @@ public class ApkProvider extends FDroidProvider { @Override public int delete(Uri uri, String where, String[] whereArgs) { + QuerySelection query = new QuerySelection(where, whereArgs); + switch (matcher.match(uri)) { case CODE_LIST: // Don't support deleting of multiple apks yet. return 0; + case CODE_REPO: + query = query.add(queryRepo(Long.parseLong(uri.getLastPathSegment()))); + break; + case CODE_SINGLE: - where = appendPrimaryKeyToSelection(where); - whereArgs = appendPrimaryKeyToArgs(uri, whereArgs); + query = query.add(querySingle(uri)); break; default: @@ -341,7 +470,7 @@ public class ApkProvider extends FDroidProvider { throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri); } - int rowsAffected = write().delete(getTableName(), where, whereArgs); + int rowsAffected = write().delete(getTableName(), query.getSelection(), query.getArgs()); getContext().getContentResolver().notifyChange(uri, null); return rowsAffected; @@ -350,19 +479,22 @@ public class ApkProvider extends FDroidProvider { @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { + QuerySelection query = new QuerySelection(where, whereArgs); + switch (matcher.match(uri)) { case CODE_LIST: return 0; case CODE_SINGLE: - where = appendPrimaryKeyToSelection(where); - whereArgs = appendPrimaryKeyToArgs(uri, whereArgs); + query = query.add(querySingle(uri)); break; } removeRepoFields(values); - int numRows = write().update(getTableName(), values, where, whereArgs); - getContext().getContentResolver().notifyChange(uri, null); + int numRows = write().update(getTableName(), values, query.getSelection(), query.getArgs()); + if (!isApplyingBatch()) { + getContext().getContentResolver().notifyChange(uri, null); + } return numRows; } diff --git a/src/org/fdroid/fdroid/data/App.java b/src/org/fdroid/fdroid/data/App.java new file mode 100644 index 000000000..cec9b452f --- /dev/null +++ b/src/org/fdroid/fdroid/data/App.java @@ -0,0 +1,230 @@ +package org.fdroid.fdroid.data; + +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.database.Cursor; +import org.fdroid.fdroid.AppFilter; +import org.fdroid.fdroid.Utils; + +import java.util.Date; +import java.util.Map; + +public class App extends ValueObject implements Comparable { + + // True if compatible with the device (i.e. if at least one apk is) + public boolean compatible; + + public String id = "unknown"; + public String name = "Unknown"; + public String summary = "Unknown application"; + public String icon; + + public String description; + + public String license = "Unknown"; + + public String webURL; + + public String trackerURL; + + public String sourceURL; + + public String donateURL; + + public String bitcoinAddr; + + public String litecoinAddr; + + public String dogecoinAddr; + + public String flattrID; + + public String curVersion; + public int curVercode; + public Date added; + public Date lastUpdated; + + // List of categories (as defined in the metadata + // documentation) or null if there aren't any. + public Utils.CommaSeparatedList categories; + + // List of anti-features (as defined in the metadata + // documentation) or null if there aren't any. + public Utils.CommaSeparatedList antiFeatures; + + // List of special requirements (such as root privileges) or + // null if there aren't any. + public Utils.CommaSeparatedList requirements; + + // True if all updates for this app are to be ignored + public boolean ignoreAllUpdates; + + // True if the current update for this app is to be ignored + public int ignoreThisUpdate; + + // Used internally for tracking during repo updates. + public boolean updated; + + public String iconUrl; + + @Override + public int compareTo(App app) { + return name.compareToIgnoreCase(app.name); + } + + public App() { + + } + + public App(Cursor cursor) { + + checkCursorPosition(cursor); + + for(int i = 0; i < cursor.getColumnCount(); i ++ ) { + String column = cursor.getColumnName(i); + if (column.equals(AppProvider.DataColumns.IS_COMPATIBLE)) { + compatible = cursor.getInt(i) == 1; + } else if (column.equals(AppProvider.DataColumns.APP_ID)) { + id = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.NAME)) { + name = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.SUMMARY)) { + summary = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.ICON)) { + icon = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.DESCRIPTION)) { + description = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.LICENSE)) { + license = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.WEB_URL)) { + webURL = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.TRACKER_URL)) { + trackerURL = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.SOURCE_URL)) { + sourceURL = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.DONATE_URL)) { + donateURL = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.BITCOIN_ADDR)) { + bitcoinAddr = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.LITECOIN_ADDR)) { + litecoinAddr = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.DOGECOIN_ADDR)) { + dogecoinAddr = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.FLATTR_ID)) { + flattrID = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.CURRENT_VERSION)) { + curVersion = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.CURRENT_VERSION_CODE)) { + curVercode = cursor.getInt(i); + } else if (column.equals(AppProvider.DataColumns.ADDED)) { + added = ValueObject.toDate(cursor.getString(i)); + } else if (column.equals(AppProvider.DataColumns.LAST_UPDATED)) { + lastUpdated = ValueObject.toDate(cursor.getString(i)); + } else if (column.equals(AppProvider.DataColumns.CATEGORIES)) { + categories = Utils.CommaSeparatedList.make(cursor.getString(i)); + } else if (column.equals(AppProvider.DataColumns.ANTI_FEATURES)) { + antiFeatures = Utils.CommaSeparatedList.make(cursor.getString(i)); + } else if (column.equals(AppProvider.DataColumns.REQUIREMENTS)) { + requirements = Utils.CommaSeparatedList.make(cursor.getString(i)); + } else if (column.equals(AppProvider.DataColumns.IGNORE_ALLUPDATES)) { + ignoreAllUpdates = cursor.getInt(i) == 1; + } else if (column.equals(AppProvider.DataColumns.IGNORE_THISUPDATE)) { + ignoreThisUpdate = cursor.getInt(i); + } else if (column.equals(AppProvider.DataColumns.ICON_URL)) { + iconUrl = cursor.getString(i); + } + } + } + + public ContentValues toContentValues() { + + ContentValues values = new ContentValues(); + values.put(AppProvider.DataColumns.APP_ID, id); + values.put(AppProvider.DataColumns.NAME, name); + values.put(AppProvider.DataColumns.SUMMARY, summary); + values.put(AppProvider.DataColumns.ICON, icon); + values.put(AppProvider.DataColumns.ICON_URL, iconUrl); + values.put(AppProvider.DataColumns.DESCRIPTION, description); + values.put(AppProvider.DataColumns.LICENSE, license); + values.put(AppProvider.DataColumns.WEB_URL, webURL); + values.put(AppProvider.DataColumns.TRACKER_URL, trackerURL); + values.put(AppProvider.DataColumns.SOURCE_URL, sourceURL); + values.put(AppProvider.DataColumns.DONATE_URL, donateURL); + values.put(AppProvider.DataColumns.BITCOIN_ADDR, bitcoinAddr); + values.put(AppProvider.DataColumns.LITECOIN_ADDR, litecoinAddr); + values.put(AppProvider.DataColumns.DOGECOIN_ADDR, dogecoinAddr); + values.put(AppProvider.DataColumns.FLATTR_ID, flattrID); + values.put(AppProvider.DataColumns.ADDED, added == null ? "" : Utils.DATE_FORMAT.format(added)); + values.put(AppProvider.DataColumns.LAST_UPDATED, added == null ? "" : Utils.DATE_FORMAT.format(lastUpdated)); + values.put(AppProvider.DataColumns.CURRENT_VERSION, curVersion); + values.put(AppProvider.DataColumns.CURRENT_VERSION_CODE, curVercode); + values.put(AppProvider.DataColumns.CATEGORIES, Utils.CommaSeparatedList.str(categories)); + values.put(AppProvider.DataColumns.ANTI_FEATURES, Utils.CommaSeparatedList.str(antiFeatures)); + values.put(AppProvider.DataColumns.REQUIREMENTS, Utils.CommaSeparatedList.str(requirements)); + values.put(AppProvider.DataColumns.IS_COMPATIBLE, compatible ? 1 : 0); + values.put(AppProvider.DataColumns.IGNORE_ALLUPDATES, ignoreAllUpdates ? 1 : 0); + values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreThisUpdate); + values.put(AppProvider.DataColumns.ICON_URL, iconUrl); + + return values; + } + + /** + * Version string for the currently installed version of this apk. + * If not installed, returns null. + */ + public String getInstalledVersion(Context context) { + PackageInfo info = getInstalledInfo(context); + return info == null ? null : info.versionName; + } + + /** + * Version code for the currently installed version of this apk. + * If not installed, it returns -1. + */ + public int getInstalledVerCode(Context context) { + PackageInfo info = getInstalledInfo(context); + return info == null ? -1 : info.versionCode; + } + + /** + * True if installed by the user, false if a system apk or not installed. + */ + public boolean getUserInstalled(Context context) { + PackageInfo info = getInstalledInfo(context); + return info != null && ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 1); + } + + public PackageInfo getInstalledInfo(Context context) { + Map installed = Utils.getInstalledApps(context); + return installed.containsKey(id) ? installed.get(id) : null; + } + + /** + * True if there are new versions (apks) available + */ + public boolean hasUpdates(Context context) { + boolean updates = false; + if (curVercode > 0) { + int installedVerCode = getInstalledVerCode(context); + updates = (installedVerCode > 0 && installedVerCode < curVercode); + } + return updates; + } + + // True if there are new versions (apks) available and the user wants + // to be notified about them + public boolean canAndWantToUpdate(Context context) { + boolean canUpdate = hasUpdates(context); + boolean wantsUpdate = !ignoreAllUpdates && ignoreThisUpdate < curVercode; + return canUpdate && wantsUpdate && !isFiltered(); + } + + // Whether the app is filtered or not based on AntiFeatures and root + // permission (set in the Settings page) + public boolean isFiltered() { + return new AppFilter().filter(this); + } +} diff --git a/src/org/fdroid/fdroid/data/AppProvider.java b/src/org/fdroid/fdroid/data/AppProvider.java new file mode 100644 index 000000000..e09b2c5a7 --- /dev/null +++ b/src/org/fdroid/fdroid/data/AppProvider.java @@ -0,0 +1,481 @@ +package org.fdroid.fdroid.data; + +import android.content.*; +import android.content.pm.PackageInfo; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.Utils; + +import java.util.*; + +public class AppProvider extends FDroidProvider { + + /** + * @see org.fdroid.fdroid.data.ApkProvider.MAX_APKS_TO_QUERY + */ + public static final int MAX_APPS_TO_QUERY = 900; + + public static final class Helper { + + private Helper() {} + + public static List all(ContentResolver resolver) { + return all(resolver, DataColumns.ALL); + } + + public static List all(ContentResolver resolver, String[] projection) { + Uri uri = AppProvider.getContentUri(); + Cursor cursor = resolver.query(uri, projection, null, null, null); + return cursorToList(cursor); + } + + private static List cursorToList(Cursor cursor) { + List apps = new ArrayList(); + if (cursor != null) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + apps.add(new App(cursor)); + cursor.moveToNext(); + } + cursor.close(); + } + return apps; + } + + public static String getCategoryAll(Context context) { + return context.getString(R.string.category_all); + } + + public static String getCategoryWhatsNew(Context context) { + return context.getString(R.string.category_whatsnew); + } + + public static String getCategoryRecentlyUpdated(Context context) { + return context.getString(R.string.category_recentlyupdated); + } + + public static List categories(Context context) { + ContentResolver resolver = context.getContentResolver(); + Uri uri = getContentUri(); + String[] projection = { "DISTINCT " + DataColumns.CATEGORIES }; + Cursor cursor = resolver.query(uri, projection, null, null, null ); + Set categorySet = new HashSet(); + if (cursor != null) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + for( String s : Utils.CommaSeparatedList.make(cursor.getString(0))) { + categorySet.add(s); + } + cursor.moveToNext(); + } + } + List categories = new ArrayList(categorySet); + Collections.sort(categories); + + // Populate the category list with the real categories, and the + // locally generated meta-categories for "All", "What's New" and + // "Recently Updated"... + categories.add(0, getCategoryRecentlyUpdated(context)); + categories.add(0, getCategoryWhatsNew(context)); + categories.add(0, getCategoryAll(context)); + + return categories; + } + + public static App findById(ContentResolver resolver, String appId) { + return findById(resolver, appId, DataColumns.ALL); + } + + public static App findById(ContentResolver resolver, String appId, + String[] projection) { + Uri uri = getContentUri(appId); + Cursor cursor = resolver.query(uri, projection, null, null, null); + if (cursor != null && cursor.getCount() > 0) { + cursor.moveToFirst(); + return new App(cursor); + } else { + return null; + } + } + + public static void deleteAppsWithNoApks(ContentResolver resolver) { + } + } + + public interface DataColumns { + + public static final String _ID = "rowid as _id"; + public static final String _COUNT = "_count"; + public static final String IS_COMPATIBLE = "compatible"; + public static final String APP_ID = "id"; + public static final String NAME = "name"; + public static final String SUMMARY = "summary"; + public static final String ICON = "icon"; + public static final String DESCRIPTION = "description"; + public static final String LICENSE = "license"; + public static final String WEB_URL = "webURL"; + public static final String TRACKER_URL = "trackerURL"; + public static final String SOURCE_URL = "sourceURL"; + public static final String DONATE_URL = "donateURL"; + public static final String BITCOIN_ADDR = "bitcoinAddr"; + public static final String LITECOIN_ADDR = "litecoinAddr"; + public static final String DOGECOIN_ADDR = "dogecoinAddr"; + public static final String FLATTR_ID = "flattrID"; + public static final String CURRENT_VERSION = "curVersion"; + public static final String CURRENT_VERSION_CODE = "curVercode"; + public static final String CURRENT_APK = null; + public static final String ADDED = "added"; + public static final String LAST_UPDATED = "lastUpdated"; + public static final String INSTALLED_VERSION = null; + public static final String INSTALLED_VERCODE = null; + public static final String USER_INSTALLED = null; + public static final String CATEGORIES = "categories"; + public static final String ANTI_FEATURES = "antiFeatures"; + public static final String REQUIREMENTS = "requirements"; + public static final String FILTERED = null; + public static final String HAS_UPDATES = null; + public static final String TO_UPDATE = null; + public static final String IGNORE_ALLUPDATES = "ignoreAllUpdates"; + public static final String IGNORE_THISUPDATE = "ignoreThisUpdate"; + public static final String ICON_URL = "iconUrl"; + public static final String UPDATED = null; + public static final String APKS = null; + + public static String[] ALL = { + IS_COMPATIBLE, APP_ID, NAME, SUMMARY, ICON, DESCRIPTION, + LICENSE, WEB_URL, TRACKER_URL, SOURCE_URL, DONATE_URL, + BITCOIN_ADDR, LITECOIN_ADDR, DOGECOIN_ADDR, FLATTR_ID, + CURRENT_VERSION, CURRENT_VERSION_CODE, ADDED, LAST_UPDATED, + CATEGORIES, ANTI_FEATURES, REQUIREMENTS, IGNORE_ALLUPDATES, + IGNORE_THISUPDATE, ICON_URL + }; + } + + private static final String PROVIDER_NAME = "AppProvider"; + + private static final UriMatcher matcher = new UriMatcher(-1); + + private static final String PATH_INSTALLED = "installed"; + private static final String PATH_CAN_UPDATE = "canUpdate"; + private static final String PATH_SEARCH = "search"; + private static final String PATH_NO_APKS = "noApks"; + private static final String PATH_APPS = "apps"; + private static final String PATH_RECENTLY_UPDATED = "recentlyUpdated"; + private static final String PATH_NEWLY_ADDED = "newlyAdded"; + private static final String PATH_CATEGORY = "category"; + + private static final int CAN_UPDATE = CODE_SINGLE + 1; + private static final int INSTALLED = CAN_UPDATE + 1; + private static final int SEARCH = INSTALLED + 1; + private static final int NO_APKS = SEARCH + 1; + private static final int APPS = NO_APKS + 1; + private static final int RECENTLY_UPDATED = APPS + 1; + private static final int NEWLY_ADDED = RECENTLY_UPDATED + 1; + private static final int CATEGORY = NEWLY_ADDED + 1; + + static { + matcher.addURI(getAuthority(), null, CODE_LIST); + matcher.addURI(getAuthority(), PATH_RECENTLY_UPDATED, RECENTLY_UPDATED); + matcher.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED); + matcher.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY); + matcher.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH); + matcher.addURI(getAuthority(), PATH_CAN_UPDATE, CAN_UPDATE); + matcher.addURI(getAuthority(), PATH_INSTALLED, INSTALLED); + matcher.addURI(getAuthority(), PATH_NO_APKS, NO_APKS); + matcher.addURI(getAuthority(), PATH_APPS + "/*", APPS); + matcher.addURI(getAuthority(), "*", CODE_SINGLE); + } + + public static Uri getContentUri() { + return Uri.parse("content://" + getAuthority()); + } + + public static Uri getRecentlyUpdatedUri() { + return Uri.withAppendedPath(getContentUri(), PATH_RECENTLY_UPDATED); + } + + public static Uri getNewlyAddedUri() { + return Uri.withAppendedPath(getContentUri(), PATH_NEWLY_ADDED); + } + + public static Uri getCategoryUri(String category) { + return getContentUri().buildUpon() + .appendPath(PATH_CATEGORY) + .appendPath(category) + .build(); + } + + public static Uri getNoApksUri() { + return Uri.withAppendedPath(getContentUri(), PATH_NO_APKS); + } + + public static Uri getInstalledUri() { + return Uri.withAppendedPath(getContentUri(), PATH_INSTALLED); + } + + public static Uri getCanUpdateUri() { + return Uri.withAppendedPath(getContentUri(), PATH_CAN_UPDATE); + } + + public static Uri getContentUri(List apps) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < apps.size(); i ++) { + if (i != 0) { + builder.append(','); + } + builder.append(apps.get(i).id); + } + return getContentUri().buildUpon() + .appendPath(PATH_APPS) + .appendPath(builder.toString()) + .build(); + } + + public static Uri getContentUri(App app) { + return getContentUri(app.id); + } + + public static Uri getContentUri(String appId) { + return Uri.withAppendedPath(getContentUri(), appId); + } + + public static Uri getSearchUri(String query) { + return getContentUri().buildUpon() + .appendPath(PATH_SEARCH) + .appendPath(query) + .build(); + } + + @Override + protected String getTableName() { + return DBHelper.TABLE_APP; + } + + @Override + protected String getProviderName() { + return "AppProvider"; + } + + public static String getAuthority() { + return AUTHORITY + "." + PROVIDER_NAME; + } + + protected UriMatcher getMatcher() { + return matcher; + } + + private QuerySelection queryCanUpdate() { + Map installedApps = Utils.getInstalledApps(getContext()); + + String ignoreCurrent = " ignoreThisUpdate != curVercode "; + String ignoreAll = " ignoreAllUpdates != 1 "; + String ignore = " ( " + ignoreCurrent + " AND " + ignoreAll + " ) "; + + StringBuilder where = new StringBuilder( ignore + " AND ( 0 "); + String[] selectionArgs = new String[installedApps.size() * 2]; + int i = 0; + for (PackageInfo info : installedApps.values() ) { + where.append(" OR ( ") + .append(AppProvider.DataColumns.APP_ID) + .append(" = ? AND ") + .append(DataColumns.CURRENT_VERSION_CODE) + .append(" > ?) "); + selectionArgs[ i * 2 ] = info.packageName; + selectionArgs[ i * 2 + 1 ] = Integer.toString(info.versionCode); + i ++; + } + where.append(") "); + + return new QuerySelection(where.toString(), selectionArgs); + } + + private QuerySelection queryInstalled() { + Map installedApps = Utils.getInstalledApps(getContext()); + StringBuilder where = new StringBuilder( " ( 0 "); + String[] selectionArgs = new String[installedApps.size()]; + int i = 0; + for (Map.Entry entry : installedApps.entrySet() ) { + where.append(" OR ") + .append(AppProvider.DataColumns.APP_ID) + .append(" = ? "); + selectionArgs[i] = entry.getKey(); + i ++; + } + where.append(" ) "); + + return new QuerySelection(where.toString(), selectionArgs); + } + + private QuerySelection querySearch(String keywords) { + keywords = "%" + keywords + "%"; + String selection = + "id like ? OR " + + "name like ? OR " + + "summary like ? OR " + + "description like ? "; + String[] args = new String[] { keywords, keywords, keywords, keywords}; + return new QuerySelection(selection, args); + } + + private QuerySelection queryNewlyAdded() { + String selection = "added > ?"; + String[] args = new String[] { + Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory()) + }; + return new QuerySelection(selection, args); + } + + private QuerySelection queryRecentlyUpdated() { + String selection = "added != lastUpdated AND lastUpdated > ?"; + String[] args = new String[] { + Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory()) + }; + return new QuerySelection(selection, args); + } + + private QuerySelection queryCategory(String category) { + // TODO: In the future, add a new table for categories, + // so we can join onto it. + String selection = + " categories = ? OR " + // Only category e.g. "internet" + " categories LIKE ? OR " + // First category e.g. "internet,%" + " categories LIKE ? OR " + // Last category e.g. "%,internet" + " categories LIKE ? "; // One of many categories e.g. "%,internet,%" + String[] args = new String[] { + category, + category + ",%", + "%," + category, + "%," + category + ",%", + }; + return new QuerySelection(selection, args); + } + + private QuerySelection queryNoApks() { + String selection = "(SELECT COUNT(*) FROM fdroid_apk WHERE fdroid_apk.id = fdroid_app.id) = 0"; + return new QuerySelection(selection); + } + + private QuerySelection queryApps(String appIds) { + String[] args = appIds.split(","); + String selection = "id IN (" + generateQuestionMarksForInClause(args.length) + ")"; + return new QuerySelection(selection, args); + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + QuerySelection query = new QuerySelection(selection, selectionArgs); + switch (matcher.match(uri)) { + case CODE_LIST: + break; + + case CODE_SINGLE: + query = query.add( + DataColumns.APP_ID + " = ?", + new String[] { uri.getLastPathSegment() } ); + break; + + case CAN_UPDATE: + query = query.add(queryCanUpdate()); + break; + + case INSTALLED: + query = query.add(queryInstalled()); + break; + + case SEARCH: + query = query.add(querySearch(uri.getLastPathSegment())); + break; + + case NO_APKS: + query = query.add(queryNoApks()); + break; + + case APPS: + query = query.add(queryApps(uri.getLastPathSegment())); + break; + + case CATEGORY: + query = query.add(queryCategory(uri.getLastPathSegment())); + break; + + case RECENTLY_UPDATED: + sortOrder = DataColumns.LAST_UPDATED + " DESC"; + query = query.add(queryRecentlyUpdated()); + break; + + case NEWLY_ADDED: + sortOrder = DataColumns.ADDED + " DESC"; + query = query.add(queryNewlyAdded()); + break; + + default: + Log.e("FDroid", "Invalid URI for app content provider: " + uri); + throw new UnsupportedOperationException("Invalid URI for app content provider: " + uri); + } + + for (String field : projection) { + if (field.equals(DataColumns._COUNT)) { + projection = new String[] { "COUNT(*) AS " + DataColumns._COUNT }; + break; + } + } + + Cursor cursor = read().query(getTableName(), projection, query.getSelection(), + query.getArgs(), null, null, sortOrder); + cursor.setNotificationUri(getContext().getContentResolver(), uri); + return cursor; + } + + @Override + public int delete(Uri uri, String where, String[] whereArgs) { + + QuerySelection query = new QuerySelection(where, whereArgs); + switch (matcher.match(uri)) { + + case NO_APKS: + query = query.add(queryNoApks()); + break; + + default: + throw new UnsupportedOperationException("Can't delete yet"); + + } + + int count = write().delete(getTableName(), query.getSelection(), query.getArgs()); + getContext().getContentResolver().notifyChange(uri, null); + return count; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + long id = write().insertOrThrow(getTableName(), null, values); + if (!isApplyingBatch()) { + getContext().getContentResolver().notifyChange(uri, null); + } + return getContentUri(values.getAsString(DataColumns.APP_ID)); + } + + @Override + public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { + QuerySelection query = new QuerySelection(where, whereArgs); + switch (matcher.match(uri)) { + + case CODE_SINGLE: + query = query.add(new QuerySelection("id = ?", new String[] { uri.getLastPathSegment()})); + break; + + default: + throw new UnsupportedOperationException("Update not supported for '" + uri + "'."); + + } + int count = write().update(getTableName(), values, query.getSelection(), query.getArgs()); + if (!isApplyingBatch()) { + getContext().getContentResolver().notifyChange(uri, null); + } + return count; + } + +} diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index 58807514c..df872d2c9 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -2,12 +2,13 @@ package org.fdroid.fdroid.data; import android.content.ContentValues; import android.content.Context; +import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.preference.PreferenceManager; import android.util.Log; -import org.fdroid.fdroid.DB; -import org.fdroid.fdroid.R; +import org.fdroid.fdroid.*; import java.util.ArrayList; import java.util.List; @@ -23,6 +24,7 @@ public class DBHelper extends SQLiteOpenHelper { // This information is retrieved from the repositories. public static final String TABLE_APK = "fdroid_apk"; + private static final String CREATE_TABLE_REPO = "create table " + TABLE_REPO + " (_id integer primary key, " + "address text not null, " @@ -50,26 +52,41 @@ public class DBHelper extends SQLiteOpenHelper { + "hashType string, " + "added string, " + "compatible int not null, " + + "incompatibleReasons text, " + "primary key(id, vercode)" + ");"; - private static final String CREATE_TABLE_APP = "create table " + DB.TABLE_APP - + " ( " + "id text not null, " + "name text not null, " - + "summary text not null, " + "icon text, " - + "description text not null, " + "license text not null, " - + "webURL text, " + "trackerURL text, " + "sourceURL text, " - + "curVersion text," + "curVercode integer," - + "antiFeatures string," + "donateURL string," - + "bitcoinAddr string," + "litecoinAddr string," + public static final String TABLE_APP = "fdroid_app"; + private static final String CREATE_TABLE_APP = "CREATE TABLE " + TABLE_APP + + " ( " + + "id text not null, " + + "name text not null, " + + "summary text not null, " + + "icon text, " + + "description text not null, " + + "license text not null, " + + "webURL text, " + + "trackerURL text, " + + "sourceURL text, " + + "curVersion text," + + "curVercode integer," + + "antiFeatures string," + + "donateURL string," + + "bitcoinAddr string," + + "litecoinAddr string," + "dogecoinAddr string," - + "flattrID string," + "requirements string," - + "categories string," + "added string," - + "lastUpdated string," + "compatible int not null," + + "flattrID string," + + "requirements string," + + "categories string," + + "added string," + + "lastUpdated string," + + "compatible int not null," + "ignoreAllUpdates int not null," + "ignoreThisUpdate int not null," + + "iconUrl text, " + "primary key(id));"; - private static final int DB_VERSION = 37; + private static final int DB_VERSION = 39; private Context context; @@ -80,18 +97,20 @@ public class DBHelper extends SQLiteOpenHelper { private void populateRepoNames(SQLiteDatabase db, int oldVersion) { if (oldVersion < 37) { + Log.i("FDroid", "Populating repo names from the url"); String[] columns = { "address", "_id" }; Cursor cursor = db.query(TABLE_REPO, columns, "name IS NULL OR name = ''", null, null, null, null); - cursor.moveToFirst(); if (cursor.getCount() > 0) { cursor.moveToFirst(); while (!cursor.isAfterLast()) { String address = cursor.getString(0); long id = cursor.getInt(1); ContentValues values = new ContentValues(1); - values.put("name", Repo.addressToName(address)); + String name = Repo.addressToName(address); + values.put("name", name); String[] args = { Long.toString( id ) }; + Log.i("FDroid", "Setting repo name to '" + name + "' for repo " + address); db.update(TABLE_REPO, values, "_id = ?", args); cursor.moveToNext(); } @@ -103,7 +122,6 @@ public class DBHelper extends SQLiteOpenHelper { if (oldVersion < 36) { Log.d("FDroid", "Renaming " + TABLE_REPO + ".id to _id"); - db.beginTransaction(); try { @@ -166,7 +184,7 @@ public class DBHelper extends SQLiteOpenHelper { context.getString(R.string.default_repo_description)); values.put("version", 0); String pubkey = context.getString(R.string.default_repo_pubkey); - String fingerprint = DB.calcFingerprint(pubkey); + String fingerprint = Utils.calcFingerprint(pubkey); values.put("pubkey", pubkey); values.put("fingerprint", fingerprint); values.put("maxage", 0); @@ -296,7 +314,7 @@ public class DBHelper extends SQLiteOpenHelper { c.close(); for (Repo repo : oldrepos) { ContentValues values = new ContentValues(); - values.put("fingerprint", DB.calcFingerprint(repo.pubkey)); + values.put("fingerprint", Utils.calcFingerprint(repo.pubkey)); db.update(TABLE_REPO, values, "address = ?", new String[] { repo.address }); } } @@ -316,6 +334,7 @@ public class DBHelper extends SQLiteOpenHelper { private void addLastUpdatedToRepo(SQLiteDatabase db, int oldVersion) { if (oldVersion < 35 && !columnExists(db, TABLE_REPO, "lastUpdated")) { + Log.i("FDroid", "Adding lastUpdated column to " + TABLE_REPO); db.execSQL("Alter table " + TABLE_REPO + " add column lastUpdated string"); } } @@ -323,7 +342,7 @@ public class DBHelper extends SQLiteOpenHelper { private void resetTransient(SQLiteDatabase db) { context.getSharedPreferences("FDroid", Context.MODE_PRIVATE).edit() .putBoolean("triedEmptyUpdate", false).commit(); - db.execSQL("drop table " + DB.TABLE_APP); + db.execSQL("drop table " + TABLE_APP); db.execSQL("drop table " + TABLE_APK); db.execSQL("update " + TABLE_REPO + " set lastetag = NULL"); createAppApk(db); @@ -331,7 +350,7 @@ public class DBHelper extends SQLiteOpenHelper { private static void createAppApk(SQLiteDatabase db) { db.execSQL(CREATE_TABLE_APP); - db.execSQL("create index app_id on " + DB.TABLE_APP + " (id);"); + db.execSQL("create index app_id on " + TABLE_APP + " (id);"); db.execSQL(CREATE_TABLE_APK); db.execSQL("create index apk_vercode on " + TABLE_APK + " (vercode);"); db.execSQL("create index apk_id on " + TABLE_APK + " (id);"); diff --git a/src/org/fdroid/fdroid/data/FDroidProvider.java b/src/org/fdroid/fdroid/data/FDroidProvider.java index cd8308bd2..3e0c4b316 100644 --- a/src/org/fdroid/fdroid/data/FDroidProvider.java +++ b/src/org/fdroid/fdroid/data/FDroidProvider.java @@ -1,11 +1,12 @@ package org.fdroid.fdroid.data; -import android.content.ContentProvider; -import android.content.UriMatcher; +import android.content.*; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; -abstract class FDroidProvider extends ContentProvider { +import java.util.ArrayList; + +public abstract class FDroidProvider extends ContentProvider { public static final String AUTHORITY = "org.fdroid.fdroid.data"; @@ -14,10 +15,39 @@ abstract class FDroidProvider extends ContentProvider { private DBHelper dbHelper; + private boolean isApplyingBatch = false; + abstract protected String getTableName(); abstract protected String getProviderName(); + /** + * Tells us if we are in the middle of a batch of operations. Allows us to + * decide not to notify the content resolver of changes, + * every single time we do something during many operations. + * Based on http://stackoverflow.com/a/15886915. + * @return + */ + protected final boolean isApplyingBatch() { + return this.isApplyingBatch; + } + + @Override + public ContentProviderResult[] applyBatch(ArrayList operations) + throws OperationApplicationException { + ContentProviderResult[] result = null; + isApplyingBatch = true; + write().beginTransaction(); + try { + result = super.applyBatch(operations); + write().setTransactionSuccessful(); + } finally { + write().endTransaction(); + isApplyingBatch = false; + } + return result; + } + @Override public boolean onCreate() { dbHelper = new DBHelper(getContext()); @@ -55,5 +85,15 @@ abstract class FDroidProvider extends ContentProvider { abstract protected UriMatcher getMatcher(); + protected String generateQuestionMarksForInClause(int num) { + StringBuilder sb = new StringBuilder(num * 2); + for (int i = 0; i < num; i ++) { + if (i != 0) { + sb.append(','); + } + sb.append('?'); + } + return sb.toString(); + } } diff --git a/src/org/fdroid/fdroid/data/QuerySelection.java b/src/org/fdroid/fdroid/data/QuerySelection.java new file mode 100644 index 000000000..7c22283f2 --- /dev/null +++ b/src/org/fdroid/fdroid/data/QuerySelection.java @@ -0,0 +1,79 @@ +package org.fdroid.fdroid.data; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Helper class used by sublasses of ContentProvider to make the constraints + * required for a given content URI (e.g. all apps that belong to a repo) + * easily appendable to the constraints which are passed into, e.g. the query() + * method in the content provider. + */ +public class QuerySelection { + + private final String[] args; + private final String selection; + + public QuerySelection(String selection) { + this.selection = selection; + this.args = null; + } + + public QuerySelection(String selection, String[] args) { + this.args = args; + this.selection = selection; + } + + public QuerySelection(String selection, List args) { + this.args = new String[ args.size() ]; + args.toArray( this.args ); + this.selection = selection; + } + + public String[] getArgs() { + return args; + } + + public String getSelection() { + return selection; + } + + public boolean hasSelection() { + return selection != null && selection.length() > 0; + } + + public boolean hasArgs() { + return args != null && args.length > 0; + } + + public QuerySelection add(String selection, String[] args) { + return add(new QuerySelection(selection, args)); + } + + public QuerySelection add(QuerySelection query) { + String s = null; + if (this.hasSelection() && query.hasSelection()) { + s = " (" + this.selection + ") AND (" + query.getSelection() + ") "; + } else if (this.hasSelection()) { + s = this.selection; + } else if (query.hasSelection() ) { + s = query.selection; + } + + int thisNumArgs = this.hasArgs() ? this.args.length : 0; + int queryNumArgs = query.hasArgs() ? query.args.length : 0; + List a = new ArrayList(thisNumArgs + queryNumArgs); + + if (this.hasArgs()) { + Collections.addAll(a, this.args); + } + + if (query.hasArgs()) { + Collections.addAll(a, query.getArgs()); + } + + return new QuerySelection(s, a); + } + +} diff --git a/src/org/fdroid/fdroid/data/Repo.java b/src/org/fdroid/fdroid/data/Repo.java index 051c634ff..d5bc3bc56 100644 --- a/src/org/fdroid/fdroid/data/Repo.java +++ b/src/org/fdroid/fdroid/data/Repo.java @@ -3,14 +3,16 @@ package org.fdroid.fdroid.data; import android.content.ContentValues; import android.database.Cursor; import android.util.Log; -import org.fdroid.fdroid.DB; +import org.fdroid.fdroid.Utils; import java.net.MalformedURLException; import java.net.URL; import java.text.ParseException; import java.util.Date; -public class Repo extends ValueObject{ +public class Repo extends ValueObject { + + public static final int VERSION_DENSITY_SPECIFIC_ICONS = 11; private long id; @@ -31,6 +33,9 @@ public class Repo extends ValueObject{ } public Repo(Cursor cursor) { + + checkCursorPosition(cursor); + for(int i = 0; i < cursor.getColumnCount(); i ++ ) { String column = cursor.getColumnName(i); if (column.equals(RepoProvider.DataColumns._ID)) { @@ -132,7 +137,7 @@ public class Repo extends ValueObject{ String dateString = values.getAsString(RepoProvider.DataColumns.LAST_UPDATED); if (dateString != null) { try { - lastUpdated = DB.DATE_FORMAT.parse(dateString); + lastUpdated = Utils.DATE_FORMAT.parse(dateString); } catch (ParseException e) { Log.e("FDroid", "Error parsing date " + dateString); } diff --git a/src/org/fdroid/fdroid/data/RepoProvider.java b/src/org/fdroid/fdroid/data/RepoProvider.java index 6466af81f..8f9199f5c 100644 --- a/src/org/fdroid/fdroid/data/RepoProvider.java +++ b/src/org/fdroid/fdroid/data/RepoProvider.java @@ -6,8 +6,8 @@ import android.net.Uri; import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; -import org.fdroid.fdroid.DB; import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.Utils; import java.util.ArrayList; import java.util.List; @@ -15,6 +15,7 @@ import java.util.List; public class RepoProvider extends FDroidProvider { public static final class Helper { + public static final String TAG = "RepoProvider.Helper"; private Helper() {} @@ -105,7 +106,7 @@ public class RepoProvider extends FDroidProvider { */ if (values.containsKey(DataColumns.PUBLIC_KEY)) { String publicKey = values.getAsString(DataColumns.PUBLIC_KEY); - String calcedFingerprint = DB.calcFingerprint(publicKey); + String calcedFingerprint = Utils.calcFingerprint(publicKey); if (values.containsKey(DataColumns.FINGERPRINT)) { String fingerprint = values.getAsString(DataColumns.FINGERPRINT); if (!TextUtils.isEmpty(publicKey)) { @@ -153,15 +154,16 @@ public class RepoProvider extends FDroidProvider { resolver.delete(uri, null, null); } - public static void purgeApps(Repo repo, FDroidApp app) { - // TODO: Once we have content providers for apps and apks, use them - // to do this... - DB db = DB.getDB(); - try { - db.purgeApps(repo, app); - } finally { - DB.releaseDB(); - } + public static void purgeApps(Context context, Repo repo, FDroidApp app) { + Uri apkUri = ApkProvider.getRepoUri(repo.getId()); + int apkCount = context.getContentResolver().delete(apkUri, null, null); + Log.d("FDroid", "Removed " + apkCount + " apks from repo " + repo.name); + + Uri appUri = AppProvider.getNoApksUri(); + int appCount = context.getContentResolver().delete(appUri, null, null); + Log.d("Log", "Removed " + appCount + " apps with no apks."); + + app.invalidateAllApps(); } public static int countAppsForRepo(ContentResolver resolver, diff --git a/src/org/fdroid/fdroid/data/ValueObject.java b/src/org/fdroid/fdroid/data/ValueObject.java index fb936cc77..c17958d86 100644 --- a/src/org/fdroid/fdroid/data/ValueObject.java +++ b/src/org/fdroid/fdroid/data/ValueObject.java @@ -1,18 +1,27 @@ package org.fdroid.fdroid.data; +import android.database.Cursor; import android.util.Log; -import org.fdroid.fdroid.DB; +import org.fdroid.fdroid.Utils; import java.text.ParseException; import java.util.Date; abstract class ValueObject { + protected void checkCursorPosition(Cursor cursor) throws IllegalArgumentException { + if (cursor.getPosition() == -1) { + throw new IllegalArgumentException( + "Cursor position is -1. " + + "Did you forget to moveToFirst() or move() before passing to the value object?"); + } + } + static Date toDate(String string) { Date date = null; if (string != null) { try { - date = DB.DATE_FORMAT.parse(string); + date = Utils.DATE_FORMAT.parse(string); } catch (ParseException e) { Log.e("FDroid", "Error parsing date " + string); } diff --git a/src/org/fdroid/fdroid/updater/RepoUpdater.java b/src/org/fdroid/fdroid/updater/RepoUpdater.java index 0ab99ba3d..b5c17fb07 100644 --- a/src/org/fdroid/fdroid/updater/RepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/RepoUpdater.java @@ -4,10 +4,11 @@ import android.content.ContentValues; import android.content.Context; import android.os.Bundle; import android.util.Log; -import org.fdroid.fdroid.DB; import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.RepoXMLHandler; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.net.Downloader; @@ -40,7 +41,8 @@ abstract public class RepoUpdater { protected final Context context; protected final Repo repo; - protected final List apps = new ArrayList(); + private List apps = new ArrayList(); + private List apks = new ArrayList(); protected boolean hasChanged = false; protected ProgressListener progressListener; @@ -57,10 +59,14 @@ abstract public class RepoUpdater { return hasChanged; } - public List getApps() { + public List getApps() { return apps; } + public List getApks() { + return apks; + } + public boolean isInteractive() { return progressListener != null; } @@ -173,7 +179,7 @@ abstract public class RepoUpdater { // Process the index... SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); XMLReader reader = parser.getXMLReader(); - RepoXMLHandler handler = new RepoXMLHandler(repo, apps, progressListener); + RepoXMLHandler handler = new RepoXMLHandler(repo, progressListener); if (isInteractive()) { // Only bother spending the time to count the expected apps @@ -186,6 +192,8 @@ abstract public class RepoUpdater { new BufferedReader(new FileReader(indexFile))); reader.parse(is); + apps = handler.getApps(); + apks = handler.getApks(); updateRepo(handler, downloader.getETag()); } } catch (SAXException e) { @@ -216,7 +224,7 @@ abstract public class RepoUpdater { ContentValues values = new ContentValues(); - values.put(RepoProvider.DataColumns.LAST_UPDATED, DB.DATE_FORMAT.format(new Date())); + values.put(RepoProvider.DataColumns.LAST_UPDATED, Utils.DATE_FORMAT.format(new Date())); if (repo.lastetag == null || !repo.lastetag.equals(etag)) { values.put(RepoProvider.DataColumns.LAST_ETAG, etag); diff --git a/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java b/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java index d8925c304..ce22fd0c3 100644 --- a/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java @@ -2,7 +2,6 @@ package org.fdroid.fdroid.updater; import android.content.Context; import android.util.Log; -import org.fdroid.fdroid.DB; import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; @@ -30,7 +29,7 @@ public class SignedRepoUpdater extends RepoUpdater { boolean match = false; for (Certificate cert : certs) { String certdata = Hasher.hex(cert); - if (repo.pubkey == null && repo.fingerprint.equals(DB.calcFingerprint(cert))) { + if (repo.pubkey == null && repo.fingerprint.equals(Utils.calcFingerprint(cert))) { repo.pubkey = certdata; } if (repo.pubkey != null && repo.pubkey.equals(certdata)) { diff --git a/src/org/fdroid/fdroid/updater/UnsignedRepoUpdater.java b/src/org/fdroid/fdroid/updater/UnsignedRepoUpdater.java index ec9b0712b..2dac81e23 100644 --- a/src/org/fdroid/fdroid/updater/UnsignedRepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/UnsignedRepoUpdater.java @@ -2,9 +2,7 @@ package org.fdroid.fdroid.updater; import android.content.Context; import android.util.Log; -import org.fdroid.fdroid.DB; import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.net.Downloader; import java.io.File; diff --git a/src/org/fdroid/fdroid/views/AppListAdapter.java b/src/org/fdroid/fdroid/views/AppListAdapter.java index d1ad13000..3665ec53e 100644 --- a/src/org/fdroid/fdroid/views/AppListAdapter.java +++ b/src/org/fdroid/fdroid/views/AppListAdapter.java @@ -1,50 +1,60 @@ package org.fdroid.fdroid.views; -import java.util.ArrayList; -import java.util.List; - import android.content.Context; -import android.graphics.Typeface; +import android.content.pm.PackageInfo; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.support.v4.widget.CursorAdapter; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.*; -import android.text.SpannableString; -import android.text.style.StyleSpan; -import android.graphics.Bitmap; - -import org.fdroid.fdroid.DB; -import org.fdroid.fdroid.Preferences; -import org.fdroid.fdroid.R; -import org.fdroid.fdroid.compat.LayoutCompat; - -import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.ImageScaleType; +import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.App; -abstract public class AppListAdapter extends BaseAdapter { +abstract public class AppListAdapter extends CursorAdapter { - private List items = new ArrayList(); private Context mContext; private LayoutInflater mInflater; private DisplayImageOptions displayImageOptions; - public AppListAdapter(Context context) { + public AppListAdapter(Context context, Cursor c) { + super(context, c); + init(context); + } + + public AppListAdapter(Context context, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + init(context); + } + + public AppListAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + init(context); + } + + private void init(Context context) { mContext = context; mInflater = (LayoutInflater) mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE); - displayImageOptions = new DisplayImageOptions.Builder() - .cacheInMemory(true) - .cacheOnDisc(true) - .imageScaleType(ImageScaleType.NONE) - .resetViewBeforeLoading(true) - .showImageOnLoading(R.drawable.ic_repo_app_default) - .showImageForEmptyUri(R.drawable.ic_repo_app_default) - .displayer(new FadeInBitmapDisplayer(200, true, true, false)) - .bitmapConfig(Bitmap.Config.RGB_565) - .build(); + .cacheInMemory(true) + .cacheOnDisc(true) + .imageScaleType(ImageScaleType.NONE) + .resetViewBeforeLoading(true) + .showImageOnLoading(R.drawable.ic_repo_app_default) + .showImageForEmptyUri(R.drawable.ic_repo_app_default) + .displayer(new FadeInBitmapDisplayer(200, true, true, false)) + .bitmapConfig(Bitmap.Config.RGB_565) + .build(); } @@ -52,33 +62,6 @@ abstract public class AppListAdapter extends BaseAdapter { abstract protected boolean showStatusInstalled(); - public void addItem(DB.App app) { - items.add(app); - } - - public void addItems(List apps) { - items.addAll(apps); - } - - public void clear() { - items.clear(); - } - - @Override - public int getCount() { - return items.size(); - } - - @Override - public Object getItem(int position) { - return items.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - private static class ViewHolder { TextView name; TextView summary; @@ -88,26 +71,32 @@ abstract public class AppListAdapter extends BaseAdapter { } @Override - public View getView(int position, View convertView, ViewGroup parent) { + public View newView(Context context, Cursor cursor, ViewGroup parent) { + View view = mInflater.inflate(R.layout.applistitem, null); + + ViewHolder holder = new ViewHolder(); + holder.name = (TextView) view.findViewById(R.id.name); + holder.summary = (TextView) view.findViewById(R.id.summary); + holder.status = (TextView) view.findViewById(R.id.status); + holder.license = (TextView) view.findViewById(R.id.license); + holder.icon = (ImageView) view.findViewById(R.id.icon); + view.setTag(holder); + + setupView(context, view, cursor, holder); + + return view; + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + ViewHolder holder = (ViewHolder)view.getTag(); + setupView(context, view, cursor, holder); + } + + private void setupView(Context context, View view, Cursor cursor, ViewHolder holder) { + final App app = new App(cursor); boolean compact = Preferences.get().hasCompactLayout(); - DB.App app = items.get(position); - ViewHolder holder; - - if (convertView == null) { - convertView = mInflater.inflate(R.layout.applistitem, null); - - holder = new ViewHolder(); - holder.name = (TextView) convertView.findViewById(R.id.name); - holder.summary = (TextView) convertView.findViewById(R.id.summary); - holder.status = (TextView) convertView.findViewById(R.id.status); - holder.license = (TextView) convertView.findViewById(R.id.license); - holder.icon = (ImageView) convertView.findViewById(R.id.icon); - - convertView.setTag(holder); - } else { - holder = (ViewHolder) convertView.getTag(); - } holder.name.setText(app.name); holder.summary.setText(app.summary); @@ -121,47 +110,50 @@ abstract public class AppListAdapter extends BaseAdapter { // Disable it all if it isn't compatible... View[] views = { - convertView, + view, holder.status, holder.summary, holder.license, holder.name }; - for (View view : views) { - view.setEnabled(app.compatible && !app.filtered); + for (View v : views) { + v.setEnabled(app.compatible && !app.isFiltered()); } - - return convertView; } - private String ellipsize(String input, int maxLength) { + private String ellipsize(String input, int maxLength) { if (input == null || input.length() < maxLength+1) { return input; } return input.substring(0, maxLength) + "…"; } - private String getVersionInfo(DB.App app) { + private String getVersionInfo(App app) { - if (app.curApk == null) { + if (app.curVercode <= 0) { return null; } - if (app.installedVersion == null) { - return ellipsize(app.curApk.version, 12); + PackageInfo installedInfo = app.getInstalledInfo(mContext); + + if (installedInfo == null) { + return ellipsize(app.curVersion, 12); } - if (app.toUpdate && showStatusUpdate()) { - return ellipsize(app.installedVersion, 8) + - " → " + ellipsize(app.curApk.version, 8); + String installedVersionString = installedInfo.versionName; + int installedVersionCode = installedInfo.versionCode; + + if (app.canAndWantToUpdate(mContext) && showStatusUpdate()) { + return ellipsize(installedVersionString, 8) + + " → " + ellipsize(app.curVersion, 8); } - if (app.installedVerCode > 0 && showStatusInstalled()) { - return ellipsize(app.installedVersion, 12) + " ✔"; + if (installedVersionCode > 0 && showStatusInstalled()) { + return ellipsize(installedVersionString, 12) + " ✔"; } - return app.installedVersion; + return installedVersionString; } private void layoutIcon(ImageView icon, boolean compact) { diff --git a/src/org/fdroid/fdroid/views/AppListFragmentPageAdapter.java b/src/org/fdroid/fdroid/views/AppListFragmentPageAdapter.java index 2df2e7133..b05c0e940 100644 --- a/src/org/fdroid/fdroid/views/AppListFragmentPageAdapter.java +++ b/src/org/fdroid/fdroid/views/AppListFragmentPageAdapter.java @@ -1,10 +1,13 @@ package org.fdroid.fdroid.views; +import android.database.Cursor; +import android.net.Uri; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentPagerAdapter; import org.fdroid.fdroid.FDroid; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.views.fragments.AvailableAppsFragment; import org.fdroid.fdroid.views.fragments.CanUpdateAppsFragment; import org.fdroid.fdroid.views.fragments.InstalledAppsFragment; @@ -19,7 +22,20 @@ public class AppListFragmentPageAdapter extends FragmentPagerAdapter { public AppListFragmentPageAdapter(FDroid parent) { super(parent.getSupportFragmentManager()); - this.parent = parent; + this.parent = parent; + } + + private String getUpdateTabTitle() { + Uri uri = AppProvider.getCanUpdateUri(); + String[] projection = new String[] { AppProvider.DataColumns._COUNT }; + Cursor cursor = parent.getContentResolver().query(uri, projection, null, null, null); + String suffix = ""; + if (cursor != null && cursor.getCount() == 1) { + cursor.moveToFirst(); + int count = cursor.getInt(0); + suffix = " (" + count + ")"; + } + return parent.getString(R.string.tab_updates) + suffix; } @Override @@ -46,8 +62,7 @@ public class AppListFragmentPageAdapter extends FragmentPagerAdapter { case 1: return parent.getString(R.string.inst); case 2: - return parent.getString(R.string.tab_updates) + " (" - + parent.getManager().getCanUpdateAdapter().getCount() + ")"; + return getUpdateTabTitle(); default: return ""; } diff --git a/src/org/fdroid/fdroid/views/AvailableAppListAdapter.java b/src/org/fdroid/fdroid/views/AvailableAppListAdapter.java index 2b74b2d88..0a1b8e5f8 100644 --- a/src/org/fdroid/fdroid/views/AvailableAppListAdapter.java +++ b/src/org/fdroid/fdroid/views/AvailableAppListAdapter.java @@ -1,10 +1,20 @@ package org.fdroid.fdroid.views; import android.content.Context; +import android.database.Cursor; public class AvailableAppListAdapter extends AppListAdapter { - public AvailableAppListAdapter(Context context) { - super(context); + + public AvailableAppListAdapter(Context context, Cursor c) { + super(context, c); + } + + public AvailableAppListAdapter(Context context, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + } + + public AvailableAppListAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); } @Override diff --git a/src/org/fdroid/fdroid/views/CanUpdateAppListAdapter.java b/src/org/fdroid/fdroid/views/CanUpdateAppListAdapter.java index c3dcd91ae..5d5034e0c 100644 --- a/src/org/fdroid/fdroid/views/CanUpdateAppListAdapter.java +++ b/src/org/fdroid/fdroid/views/CanUpdateAppListAdapter.java @@ -1,10 +1,20 @@ package org.fdroid.fdroid.views; import android.content.Context; +import android.database.Cursor; public class CanUpdateAppListAdapter extends AppListAdapter { - public CanUpdateAppListAdapter(Context context) { - super(context); + + public CanUpdateAppListAdapter(Context context, Cursor c) { + super(context, c); + } + + public CanUpdateAppListAdapter(Context context, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + } + + public CanUpdateAppListAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); } @Override diff --git a/src/org/fdroid/fdroid/views/InstalledAppListAdapter.java b/src/org/fdroid/fdroid/views/InstalledAppListAdapter.java index bb3138e2d..1224458fc 100644 --- a/src/org/fdroid/fdroid/views/InstalledAppListAdapter.java +++ b/src/org/fdroid/fdroid/views/InstalledAppListAdapter.java @@ -1,10 +1,20 @@ package org.fdroid.fdroid.views; import android.content.Context; +import android.database.Cursor; public class InstalledAppListAdapter extends AppListAdapter { - public InstalledAppListAdapter(Context context) { - super(context); + + public InstalledAppListAdapter(Context context, Cursor c) { + super(context, c); + } + + public InstalledAppListAdapter(Context context, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + } + + public InstalledAppListAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); } @Override diff --git a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java index 2ec73b7b7..eff4a73f6 100644 --- a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -8,6 +8,7 @@ import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.text.TextUtils; +import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.compat.ActionBarCompat; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; @@ -20,6 +21,9 @@ public class RepoDetailsActivity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { + + ((FDroidApp) getApplication()).applyTheme(this); + super.onCreate(savedInstanceState); long repoId = getIntent().getLongExtra(RepoDetailsFragment.ARG_REPO_ID, 0); diff --git a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java index 98ee1d9b4..523c271e7 100644 --- a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java @@ -1,31 +1,114 @@ package org.fdroid.fdroid.views.fragments; import android.app.Activity; +import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v4.app.ListFragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ListView; -import android.support.v4.app.Fragment; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.listener.PauseOnScrollListener; import org.fdroid.fdroid.*; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.views.AppListAdapter; -import org.fdroid.fdroid.views.AppListView; -abstract class AppListFragment extends Fragment implements AdapterView.OnItemClickListener, Preferences.ChangeListener { +abstract public class AppListFragment extends ListFragment implements + AdapterView.OnItemClickListener, + Preferences.ChangeListener, + LoaderManager.LoaderCallbacks { - protected FDroid parent; + public static final String[] APP_PROJECTION = { + AppProvider.DataColumns._ID, + AppProvider.DataColumns.APP_ID, + AppProvider.DataColumns.NAME, + AppProvider.DataColumns.SUMMARY, + AppProvider.DataColumns.IS_COMPATIBLE, + AppProvider.DataColumns.LICENSE, + AppProvider.DataColumns.ICON, + AppProvider.DataColumns.ICON_URL, + AppProvider.DataColumns.CURRENT_VERSION, + AppProvider.DataColumns.CURRENT_VERSION_CODE, + AppProvider.DataColumns.REQUIREMENTS, // Needed for filtering apps that require root. + }; + + public static final String APP_SORT = AppProvider.DataColumns.NAME; + + protected AppListAdapter appAdapter; protected abstract AppListAdapter getAppListAdapter(); protected abstract String getFromTitle(); + protected abstract Uri getDataUri(); + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + // Can't do this in the onCreate view, because "onCreateView" which + // returns the list view is "called between onCreate and + // onActivityCreated" according to the docs. + getListView().setFastScrollEnabled(true); + getListView().setOnItemClickListener(this); + getListView().setOnScrollListener(new PauseOnScrollListener(ImageLoader.getInstance(), false, true)); + } + + @Override + public void onResume() { + super.onResume(); + + //Starts a new or restarts an existing Loader in this manager + getLoaderManager().restartLoader(0, null, this); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Preferences.get().registerCompactLayoutChangeListener(this); + + appAdapter = getAppListAdapter(); + + if (appAdapter.getCount() == 0) { + updateEmptyRepos(); + } + + setListAdapter(appAdapter); + } + + /** + * 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. + * However, if we have tried this at least once, then don't try to do + * it automatically again, because the repos or internet connection may + * be bad. + */ + public boolean updateEmptyRepos() { + final String TRIED_EMPTY_UPDATE = "triedEmptyUpdate"; + SharedPreferences prefs = getActivity().getPreferences(Context.MODE_PRIVATE); + boolean hasTriedEmptyUpdate = prefs.getBoolean(TRIED_EMPTY_UPDATE, false); + if (!hasTriedEmptyUpdate) { + Log.d("FDroid", "Empty app list, and we haven't done an update yet. Forcing repo update."); + prefs.edit().putBoolean(TRIED_EMPTY_UPDATE, true).commit(); + UpdateService.updateNow(getActivity()); + return true; + } else { + Log.d("FDroid", "Empty app list, but it looks like we've had an update previously. Will not force repo update."); + return false; + } } @Override @@ -34,48 +117,9 @@ abstract class AppListFragment extends Fragment implements AdapterView.OnItemCli Preferences.get().unregisterCompactLayoutChangeListener(this); } - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - try { - parent = (FDroid)activity; - } catch (ClassCastException e) { - // I know fragments are meant to be activity agnostic, but I can't - // think of a better way to share the one application list between - // all three app list fragments. - throw new RuntimeException( - "AppListFragment can only be attached to FDroid activity. " + - "Here it was attached to a " + activity.getClass() ); - } - } - - public AppListManager getAppListManager() { - return parent.getManager(); - } - - protected AppListView createPlainAppList() { - AppListView view = new AppListView(getActivity()); - ListView list = createAppListView(); - view.addView( - list, - new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - view.setAppList(list); - return view; - } - - protected ListView createAppListView() { - ListView list = new ListView(getActivity()); - list.setFastScrollEnabled(true); - list.setOnItemClickListener(this); - list.setAdapter(getAppListAdapter()); - return list; - } - @Override public void onItemClick(AdapterView parent, View view, int position, long id) { - final DB.App app = (DB.App)getAppListAdapter().getItem(position); + final App app = new App((Cursor)getListView().getItemAtPosition(position)); Intent intent = new Intent(getActivity(), AppDetails.class); intent.putExtra("appid", app.id); intent.putExtra("from", getFromTitle()); @@ -86,4 +130,22 @@ abstract class AppListFragment extends Fragment implements AdapterView.OnItemCli public void onPreferenceChange() { getAppListAdapter().notifyDataSetChanged(); } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + appAdapter.swapCursor(data); + } + + @Override + public void onLoaderReset(Loader loader) { + appAdapter.swapCursor(null); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + Uri uri = getDataUri(); + return new CursorLoader( + getActivity(), uri, APP_PROJECTION, null, null, APP_SORT); + } + } diff --git a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java index e8a50410a..3bb104b65 100644 --- a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java @@ -1,28 +1,74 @@ package org.fdroid.fdroid.views.fragments; +import android.database.Cursor; +import android.net.Uri; import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.*; - -import org.fdroid.fdroid.views.AppListAdapter; +import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.views.AppListView; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.views.AppListAdapter; +import org.fdroid.fdroid.views.AvailableAppListAdapter; -public class AvailableAppsFragment extends AppListFragment implements AdapterView.OnItemSelectedListener { +import java.util.List; + +public class AvailableAppsFragment extends AppListFragment implements + LoaderManager.LoaderCallbacks { + + private String currentCategory = null; + private AppListAdapter adapter = null; + + @Override + protected String getFromTitle() { + return "Available"; + } + + protected AppListAdapter getAppListAdapter() { + if (adapter == null) { + final AppListAdapter a = new AvailableAppListAdapter(getActivity(), null); + Preferences.get().registerUpdateHistoryListener(new Preferences.ChangeListener() { + @Override + public void onPreferenceChange() { + a.notifyDataSetChanged(); + } + }); + adapter = a; + } + return adapter; + } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - AppListView view = new AppListView(getActivity()); + LinearLayout view = new LinearLayout(getActivity()); view.setOrientation(LinearLayout.VERTICAL); + final List categories = AppProvider.Helper.categories(getActivity()); + Spinner spinner = new Spinner(getActivity()); // Giving it an ID lets the default save/restore state // functionality do its stuff. spinner.setId(R.id.categorySpinner); - spinner.setAdapter(getAppListManager().getCategoriesAdapter()); - spinner.setOnItemSelectedListener(this); + spinner.setAdapter(new ArrayAdapter(getActivity(), android.R.layout.simple_list_item_1, categories)); + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + currentCategory = categories.get(pos); + Log.d("FDroid", "Category '" + currentCategory + "' selected."); + getLoaderManager().restartLoader(0, null, AvailableAppsFragment.this); + } + @Override + public void onNothingSelected(AdapterView parent) { + currentCategory = null; + Log.d("FDroid", "Select empty category."); + getLoaderManager().restartLoader(0, null, AvailableAppsFragment.this); + } + }); + spinner.setPadding( 0, 0, 0, 0 ); view.addView( spinner, @@ -30,8 +76,10 @@ public class AvailableAppsFragment extends AppListFragment implements AdapterVie LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); - ListView list = createAppListView(); - view.setAppList(list); + ListView list = new ListView(getActivity()); + list.setId(android.R.id.list); + list.setFastScrollEnabled(true); + list.setOnItemClickListener(this); view.addView( list, new ViewGroup.LayoutParams( @@ -42,24 +90,14 @@ public class AvailableAppsFragment extends AppListFragment implements AdapterVie } @Override - public void onItemSelected(AdapterView parent, View view, int pos, - long id) { - String category = parent.getItemAtPosition(pos).toString(); - getAppListManager().setCurrentCategory(category); - } - - @Override - public void onNothingSelected(AdapterView parent) { - - } - - @Override - protected AppListAdapter getAppListAdapter() { - return getAppListManager().getAvailableAdapter(); - } - - @Override - protected String getFromTitle() { - return getAppListManager().getCurrentCategory(); + protected Uri getDataUri() { + if (currentCategory == null || currentCategory.equals(AppProvider.Helper.getCategoryAll(getActivity()))) + return AppProvider.getContentUri(); + else if (currentCategory.equals(AppProvider.Helper.getCategoryRecentlyUpdated(getActivity()))) + return AppProvider.getRecentlyUpdatedUri(); + else if (currentCategory.equals(AppProvider.Helper.getCategoryWhatsNew(getActivity()))) + return AppProvider.getNewlyAddedUri(); + else + return AppProvider.getCategoryUri(currentCategory); } } diff --git a/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java index 1b3ebe9a3..0241b3657 100644 --- a/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java @@ -1,27 +1,30 @@ package org.fdroid.fdroid.views.fragments; +import android.database.Cursor; +import android.net.Uri; import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.views.AppListAdapter; +import org.fdroid.fdroid.views.CanUpdateAppListAdapter; public class CanUpdateAppsFragment extends AppListFragment { - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return createPlainAppList(); - } - @Override protected AppListAdapter getAppListAdapter() { - return getAppListManager().getCanUpdateAdapter(); + return new CanUpdateAppListAdapter(getActivity(), null); } @Override protected String getFromTitle() { - return parent.getString(R.string.tab_updates); + return getString(R.string.tab_updates); } + + @Override + protected Uri getDataUri() { + return AppProvider.getCanUpdateUri(); + } + } diff --git a/src/org/fdroid/fdroid/views/fragments/InstalledAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/InstalledAppsFragment.java index b27bec6b1..dbcdf5cbb 100644 --- a/src/org/fdroid/fdroid/views/fragments/InstalledAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/InstalledAppsFragment.java @@ -1,27 +1,30 @@ package org.fdroid.fdroid.views.fragments; +import android.database.Cursor; +import android.net.Uri; import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.views.AppListAdapter; +import org.fdroid.fdroid.views.InstalledAppListAdapter; public class InstalledAppsFragment extends AppListFragment { - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return createPlainAppList(); - } - @Override protected AppListAdapter getAppListAdapter() { - return getAppListManager().getInstalledAdapter(); + return new InstalledAppListAdapter(getActivity(), null); } @Override protected String getFromTitle() { - return parent.getString(R.string.inst); + return getString(R.string.inst); } + + @Override + protected Uri getDataUri() { + return AppProvider.getInstalledUri(); + } + } diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 000000000..24e19e4db --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,10 @@ +/local.properties +/.classpath +/bin/ +/gen/ +/build/ +/.gradle/ +*~ +/.idea/ +/*.iml +out diff --git a/test/AndroidManifest.xml b/test/AndroidManifest.xml new file mode 100644 index 000000000..3562f038e --- /dev/null +++ b/test/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/test/ant.properties b/test/ant.properties new file mode 100644 index 000000000..7d28fd099 --- /dev/null +++ b/test/ant.properties @@ -0,0 +1,18 @@ +# This file is used to override default values used by the Ant build system. +# +# This file must be checked into Version Control Systems, as it is +# integral to the build system of your project. + +# This file is only used by the Ant script. + +# You can use this to override default values such as +# 'source.dir' for the location of your java source folder and +# 'out.dir' for the location of your output folder. + +# You can also use it define how the release builds are signed by declaring +# the following properties: +# 'key.store' for the location of your keystore and +# 'key.alias' for the name of the key to use. +# The password will be asked during the build when you use the 'release' target. + +tested.project.dir=/home/pete/code/fdroid/client diff --git a/test/build.xml b/test/build.xml new file mode 100644 index 000000000..acf244066 --- /dev/null +++ b/test/build.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/proguard-project.txt b/test/proguard-project.txt new file mode 100644 index 000000000..f2fe1559a --- /dev/null +++ b/test/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/tests/project.properties b/test/project.properties similarity index 96% rename from tests/project.properties rename to test/project.properties index 1f896ec2b..4ab125693 100644 --- a/tests/project.properties +++ b/test/project.properties @@ -11,4 +11,4 @@ #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. -target=android-4 +target=android-19 diff --git a/test/src/android/test/ProviderTestCase2MockContext.java b/test/src/android/test/ProviderTestCase2MockContext.java new file mode 100644 index 000000000..f0b64122e --- /dev/null +++ b/test/src/android/test/ProviderTestCase2MockContext.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.test; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.test.mock.MockContext; +import android.test.mock.MockContentResolver; +import android.database.DatabaseUtils; + +import java.io.File; + +/** + * This test case class provides a framework for testing a single + * {@link ContentProvider} and for testing your app code with an + * isolated content provider. Instead of using the system map of + * providers that is based on the manifests of other applications, the test + * case creates its own internal map. It then uses this map to resolve providers + * given an authority. This allows you to inject test providers and to null out + * providers that you do not want to use. + *

+ * This test case also sets up the following mock objects: + *

+ *
    + *
  • + * An {@link android.test.IsolatedContext} that stubs out Context methods that might + * affect the rest of the running system, while allowing tests to do real file and + * database work. + *
  • + *
  • + * A {@link android.test.mock.MockContentResolver} that provides the functionality of a + * regular content resolver, but uses {@link IsolatedContext}. It stubs out + * {@link ContentResolver#notifyChange(Uri, ContentObserver, boolean)} to + * prevent the test from affecting the running system. + *
  • + *
  • + * An instance of the provider under test, running in an {@link IsolatedContext}. + *
  • + *
+ *

+ * This framework is set up automatically by the base class' {@link #setUp()} method. If you + * override this method, you must call the super method as the first statement in + * your override. + *

+ *

+ * In order for their tests to be run, concrete subclasses must provide their own + * constructor with no arguments. This constructor must call + * {@link #ProviderTestCase2MockContext(Class, String)} as its first operation. + *

+ * For more information on content provider testing, please see + * Content Provider Testing. + */ +public abstract class ProviderTestCase2MockContext extends AndroidTestCase { + + Class mProviderClass; + String mProviderAuthority; + + private IsolatedContext mProviderContext; + private MockContentResolver mResolver; + + private class MockContext2 extends MockContext { + + @Override + public Resources getResources() { + return getContext().getResources(); + } + + @Override + public File getDir(String name, int mode) { + // name the directory so the directory will be separated from + // one created through the regular Context + return getContext().getDir("mockcontext2_" + name, mode); + } + + @Override + public Context getApplicationContext() { + return this; + } + } + /** + * Constructor. + * + * @param providerClass The class name of the provider under test + * @param providerAuthority The provider's authority string + */ + public ProviderTestCase2MockContext(Class providerClass, String providerAuthority) { + mProviderClass = providerClass; + mProviderAuthority = providerAuthority; + } + + private T mProvider; + + /** + * Returns the content provider created by this class in the {@link #setUp()} method. + * @return T An instance of the provider class given as a parameter to the test case class. + */ + public T getProvider() { + return mProvider; + } + + abstract protected Context createMockContext(Context delegate); + + /** + * Sets up the environment for the test fixture. + *

+ * Creates a new + * {@link android.test.mock.MockContentResolver}, a new IsolatedContext + * that isolates the provider's file operations, and a new instance of + * the provider under test within the isolated environment. + *

+ * + * @throws Exception + */ + @Override + protected void setUp() throws Exception { + super.setUp(); + + mResolver = new MockContentResolver(); + final String filenamePrefix = "test."; + RenamingDelegatingContext targetContextWrapper = new + RenamingDelegatingContext( + createMockContext(new MockContext2()), // The context that most methods are delegated to + getContext(), // The context that file methods are delegated to + filenamePrefix); + mProviderContext = new IsolatedContext(mResolver, targetContextWrapper); + + mProvider = mProviderClass.newInstance(); + mProvider.attachInfo(mProviderContext, null); + assertNotNull(mProvider); + mResolver.addProvider(mProviderAuthority, getProvider()); + } + + /** + * Tears down the environment for the test fixture. + *

+ * Calls {@link android.content.ContentProvider#shutdown()} on the + * {@link android.content.ContentProvider} represented by mProvider. + */ + @Override + protected void tearDown() throws Exception { + mProvider.shutdown(); + super.tearDown(); + } + + /** + * Gets the {@link MockContentResolver} created by this class during initialization. You + * must use the methods of this resolver to access the provider under test. + * + * @return A {@link MockContentResolver} instance. + */ + public MockContentResolver getMockContentResolver() { + return mResolver; + } + + /** + * Gets the {@link IsolatedContext} created by this class during initialization. + * @return The {@link IsolatedContext} instance + */ + public IsolatedContext getMockContext() { + return mProviderContext; + } + + /** + *

+ * Creates a new content provider of the same type as that passed to the test case class, + * with an authority name set to the authority parameter, and using an SQLite database as + * the underlying data source. The SQL statement parameter is used to create the database. + * This method also creates a new {@link MockContentResolver} and adds the provider to it. + *

+ *

+ * Both the new provider and the new resolver are put into an {@link IsolatedContext} + * that uses the targetContext parameter for file operations and a {@link MockContext} + * for everything else. The IsolatedContext prepends the filenamePrefix parameter to + * file, database, and directory names. + *

+ *

+ * This is a convenience method for creating a "mock" provider that can contain test data. + *

+ * + * @param targetContext The context to use as the basis of the IsolatedContext + * @param filenamePrefix A string that is prepended to file, database, and directory names + * @param providerClass The type of the provider being tested + * @param authority The authority string to associated with the test provider + * @param databaseName The name assigned to the database + * @param databaseVersion The version assigned to the database + * @param sql A string containing the SQL statements that are needed to create the desired + * database and its tables. The format is the same as that generated by the + * sqlite3 tool's .dump command. + * @return ContentResolver A new {@link MockContentResolver} linked to the provider + * + * @throws IllegalAccessException + * @throws InstantiationException + */ + public static ContentResolver newResolverWithContentProviderFromSql( + Context targetContext, String filenamePrefix, Class providerClass, String authority, + String databaseName, int databaseVersion, String sql) + throws IllegalAccessException, InstantiationException { + MockContentResolver resolver = new MockContentResolver(); + RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext( + new MockContext(), // The context that most methods are delegated to + targetContext, // The context that file methods are delegated to + filenamePrefix); + Context context = new IsolatedContext(resolver, targetContextWrapper); + DatabaseUtils.createDbFromSqlStatements(context, databaseName, databaseVersion, sql); + + T provider = providerClass.newInstance(); + provider.attachInfo(context, null); + resolver.addProvider(authority, provider); + + return resolver; + } +} diff --git a/test/src/mock/MockContextEmptyComponents.java b/test/src/mock/MockContextEmptyComponents.java new file mode 100644 index 000000000..eb962bbe9 --- /dev/null +++ b/test/src/mock/MockContextEmptyComponents.java @@ -0,0 +1,14 @@ +package mock; + +/** + * As more components are required to test different parts of F-Droid, we can + * create them and add them here (and accessors to the parent class). + */ +public class MockContextEmptyComponents extends MockContextSwappableComponents { + + public MockContextEmptyComponents() { + setPackageManager(new MockEmptyPackageManager()); + setResources(new MockEmptyResources()); + } + +} diff --git a/test/src/mock/MockContextSwappableComponents.java b/test/src/mock/MockContextSwappableComponents.java new file mode 100644 index 000000000..7fcbbf971 --- /dev/null +++ b/test/src/mock/MockContextSwappableComponents.java @@ -0,0 +1,32 @@ +package mock; + +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.test.mock.MockContext; + +public class MockContextSwappableComponents extends MockContext { + + private PackageManager packageManager; + + private Resources resources; + + public MockContextSwappableComponents setPackageManager(PackageManager pm) { + packageManager = pm; + return this; + } + + public MockContextSwappableComponents setResources(Resources resources) { + this.resources = resources; + return this; + } + + @Override + public PackageManager getPackageManager() { + return packageManager; + } + + @Override + public Resources getResources() { + return resources; + } +} diff --git a/test/src/mock/MockEmptyPackageManager.java b/test/src/mock/MockEmptyPackageManager.java new file mode 100644 index 000000000..39fdee310 --- /dev/null +++ b/test/src/mock/MockEmptyPackageManager.java @@ -0,0 +1,16 @@ +package mock; + +import android.content.pm.PackageInfo; +import android.test.mock.MockPackageManager; + +import java.util.ArrayList; +import java.util.List; + +public class MockEmptyPackageManager extends MockPackageManager { + + @Override + public List getInstalledPackages(int flags) { + return new ArrayList(); + } + +} diff --git a/test/src/mock/MockEmptyResources.java b/test/src/mock/MockEmptyResources.java new file mode 100644 index 000000000..fdc06e47f --- /dev/null +++ b/test/src/mock/MockEmptyResources.java @@ -0,0 +1,12 @@ +package mock; + +import android.test.mock.MockResources; + +public class MockEmptyResources extends MockResources { + + @Override + public String getString(int id) { + return ""; + } + +} diff --git a/test/src/mock/MockInstallablePackageManager.java b/test/src/mock/MockInstallablePackageManager.java new file mode 100644 index 000000000..bd47d86f4 --- /dev/null +++ b/test/src/mock/MockInstallablePackageManager.java @@ -0,0 +1,26 @@ +package mock; + +import android.content.pm.PackageInfo; +import android.test.mock.MockPackageManager; + +import java.util.ArrayList; +import java.util.List; + +public class MockInstallablePackageManager extends MockPackageManager { + + private List info = new ArrayList(); + + @Override + public List getInstalledPackages(int flags) { + return info; + } + + public void install(String id, int version, String versionName) { + PackageInfo p = new PackageInfo(); + p.packageName = id; + p.versionCode = version; + p.versionName = versionName; + info.add(p); + } + +} diff --git a/test/src/org/fdroid/fdroid/AppProviderTest.java b/test/src/org/fdroid/fdroid/AppProviderTest.java new file mode 100644 index 000000000..c1ccb7b9c --- /dev/null +++ b/test/src/org/fdroid/fdroid/AppProviderTest.java @@ -0,0 +1,138 @@ +package org.fdroid.fdroid; + +import android.content.ContentValues; +import android.content.pm.PackageInfo; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; +import mock.MockInstallablePackageManager; +import org.fdroid.fdroid.data.ApkProvider; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class AppProviderTest extends FDroidProviderTest { + + public AppProviderTest() { + super(AppProvider.class, AppProvider.getAuthority()); + } + + protected String[] getMinimalProjection() { + return new String[] { + AppProvider.DataColumns.APP_ID, + AppProvider.DataColumns.NAME + }; + } + + public void testUris() { + assertInvalidUri(AppProvider.getAuthority()); + assertInvalidUri(ApkProvider.getContentUri()); + + assertValidUri(AppProvider.getContentUri()); + assertValidUri(AppProvider.getSearchUri("'searching!'")); + assertValidUri(AppProvider.getNoApksUri()); + assertValidUri(AppProvider.getInstalledUri()); + assertValidUri(AppProvider.getCanUpdateUri()); + + App app = new App(); + app.id = "org.fdroid.fdroid"; + + List apps = new ArrayList(1); + apps.add(app); + + assertValidUri(AppProvider.getContentUri(app)); + assertValidUri(AppProvider.getContentUri(apps)); + assertValidUri(AppProvider.getContentUri("org.fdroid.fdroid")); + } + + public void testQuery() { + Cursor cursor = queryAllApps(); + assertNotNull(cursor); + } + + public void testInstalled() { + + Utils.clearInstalledApksCache(); + + MockInstallablePackageManager pm = new MockInstallablePackageManager(); + getSwappableContext().setPackageManager(pm); + + for (int i = 0; i < 100; i ++) { + insertApp("com.example.test." + i, "Test app " + i); + } + + assertAppCount(100, AppProvider.getContentUri()); + assertAppCount(0, AppProvider.getInstalledUri()); + + for (int i = 10; i < 20; i ++) { + pm.install("com.example.test." + i, i, "v1"); + } + + assertAppCount(10, AppProvider.getInstalledUri()); + } + + private void assertAppCount(int expectedCount, Uri uri) { + Cursor cursor = getProvider().query(uri, getMinimalProjection(), null, null, null); + assertNotNull(cursor); + assertEquals(expectedCount, cursor.getCount()); + } + + public void testInsert() { + + // Start with an empty database... + Cursor cursor = queryAllApps(); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + + // Insert a new record... + insertApp("org.fdroid.fdroid", "F-Droid"); + cursor = queryAllApps(); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + + // We intentionally throw an IllegalArgumentException if you haven't + // yet called cursor.move*()... + try { + new App(cursor); + fail(); + } catch (IllegalArgumentException e) { + // Success! + } catch (Exception e) { + fail(); + } + + // And now we should be able to recover these values from the app + // value object (because the queryAllApps() helper asks for NAME and + // APP_ID. + cursor.moveToFirst(); + App app = new App(cursor); + assertEquals("org.fdroid.fdroid", app.id); + assertEquals("F-Droid", app.name); + } + + private Cursor queryAllApps() { + return getProvider().query(AppProvider.getContentUri(), getMinimalProjection(), null, null, null); + } + + private void insertApp(String id, String name) { + ContentValues values = new ContentValues(2); + values.put(AppProvider.DataColumns.APP_ID, id); + values.put(AppProvider.DataColumns.NAME, name); + + // Required fields (NOT NULL in the database). + values.put(AppProvider.DataColumns.SUMMARY, "test summary"); + values.put(AppProvider.DataColumns.DESCRIPTION, "test description"); + values.put(AppProvider.DataColumns.LICENSE, "GPL?"); + values.put(AppProvider.DataColumns.IS_COMPATIBLE, 1); + values.put(AppProvider.DataColumns.IGNORE_ALLUPDATES, 0); + values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, 0); + + Uri uri = AppProvider.getContentUri(); + + getProvider().insert(uri, values); + } + +} diff --git a/test/src/org/fdroid/fdroid/FDroidProviderTest.java b/test/src/org/fdroid/fdroid/FDroidProviderTest.java new file mode 100644 index 000000000..eaaa84e57 --- /dev/null +++ b/test/src/org/fdroid/fdroid/FDroidProviderTest.java @@ -0,0 +1,73 @@ +package org.fdroid.fdroid; + +import android.annotation.TargetApi; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.provider.ContactsContract; +import android.test.ProviderTestCase2MockContext; +import mock.MockContextEmptyComponents; +import mock.MockContextSwappableComponents; +import org.fdroid.fdroid.data.FDroidProvider; +import org.fdroid.fdroid.mock.MockInstalledApkCache; + +public abstract class FDroidProviderTest extends ProviderTestCase2MockContext { + + private MockContextSwappableComponents swappableContext; + + public FDroidProviderTest(Class providerClass, String providerAuthority) { + super(providerClass, providerAuthority); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + Utils.setupInstalledApkCache(new MockInstalledApkCache()); + } + + @TargetApi(Build.VERSION_CODES.ECLAIR) + public void testObviouslyInvalidUris() { + assertInvalidUri("http://www.google.com"); + assertInvalidUri(ContactsContract.AUTHORITY_URI); + assertInvalidUri("junk"); + } + + @Override + protected Context createMockContext(Context delegate) { + swappableContext = new MockContextEmptyComponents(); + return swappableContext; + } + + public MockContextSwappableComponents getSwappableContext() { + return swappableContext; + } + + protected void assertInvalidUri(String uri) { + assertInvalidUri(Uri.parse(uri)); + } + + protected void assertValidUri(String uri) { + assertValidUri(Uri.parse(uri)); + } + + protected void assertInvalidUri(Uri uri) { + try { + getProvider().query(uri, getMinimalProjection(), null, null, null); + fail(); + } catch (UnsupportedOperationException e) {} + } + + protected void assertValidUri(Uri uri) { + Cursor cursor = getProvider().query(uri, getMinimalProjection(), null, null, null); + assertNotNull(cursor); + } + + /** + * Many queries need at least some sort of projection in order to produce + * valid SQL. As such, we also need to know about that, so we can provide + * helper functions that revolve around the contnet provider under test. + */ + protected abstract String[] getMinimalProjection(); + +} diff --git a/test/src/org/fdroid/fdroid/FDroidTest.java b/test/src/org/fdroid/fdroid/FDroidTest.java new file mode 100644 index 000000000..07d9f3ee3 --- /dev/null +++ b/test/src/org/fdroid/fdroid/FDroidTest.java @@ -0,0 +1,14 @@ +package org.fdroid.fdroid; + +import android.annotation.TargetApi; +import android.os.Build; +import android.test.ActivityInstrumentationTestCase2; + +@TargetApi(Build.VERSION_CODES.CUPCAKE) +public class FDroidTest extends ActivityInstrumentationTestCase2 { + + public FDroidTest() { + super("org.fdroid.fdroid", FDroid.class); + } + +} diff --git a/test/src/org/fdroid/fdroid/mock/MockInstalledApkCache.java b/test/src/org/fdroid/fdroid/mock/MockInstalledApkCache.java new file mode 100644 index 000000000..acac7557f --- /dev/null +++ b/test/src/org/fdroid/fdroid/mock/MockInstalledApkCache.java @@ -0,0 +1,16 @@ +package org.fdroid.fdroid.mock; + +import android.content.Context; +import android.content.pm.PackageInfo; +import org.fdroid.fdroid.Utils; + +import java.util.Map; + +public class MockInstalledApkCache extends Utils.InstalledApkCache { + + @Override + public Map getApks(Context context) { + return buildAppList(context); + } + +} diff --git a/tests/gen/org/fdroid/fdroid/tests/BuildConfig.java b/tests/gen/org/fdroid/fdroid/tests/BuildConfig.java deleted file mode 100644 index 2892c52fb..000000000 --- a/tests/gen/org/fdroid/fdroid/tests/BuildConfig.java +++ /dev/null @@ -1,8 +0,0 @@ -/*___Generated_by_IDEA___*/ - -/** Automatically generated file. DO NOT MODIFY */ -package org.fdroid.fdroid.tests; - -public final class BuildConfig { - public final static boolean DEBUG = true; -} \ No newline at end of file diff --git a/tests/gen/org/fdroid/fdroid/tests/Manifest.java b/tests/gen/org/fdroid/fdroid/tests/Manifest.java deleted file mode 100644 index 15e60435f..000000000 --- a/tests/gen/org/fdroid/fdroid/tests/Manifest.java +++ /dev/null @@ -1,7 +0,0 @@ -/*___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 { -} \ No newline at end of file diff --git a/tests/gen/org/fdroid/fdroid/tests/R.java b/tests/gen/org/fdroid/fdroid/tests/R.java deleted file mode 100644 index 40f6d3574..000000000 --- a/tests/gen/org/fdroid/fdroid/tests/R.java +++ /dev/null @@ -1,7 +0,0 @@ -/*___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 { -} \ No newline at end of file diff --git a/tests/local.properties b/tests/local.properties deleted file mode 100644 index 12a01149a..000000000 --- a/tests/local.properties +++ /dev/null @@ -1,10 +0,0 @@ -# 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 From 87c5ff56b83344872010fde4d09afc91ce3b58c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 9 Feb 2014 16:34:36 +0100 Subject: [PATCH 063/282] Fix gradle test root --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e9e9d7c6d..5fcbf2203 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ android { assets.srcDirs = ['assets'] } - instrumentTest.setRoot('tests') + instrumentTest.setRoot('test') } buildTypes { From 292cc40bc438e876ef653bc7d4e1f05448e24086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 9 Feb 2014 16:35:02 +0100 Subject: [PATCH 064/282] Tabbing fixes --- src/org/fdroid/fdroid/Preferences.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/org/fdroid/fdroid/Preferences.java b/src/org/fdroid/fdroid/Preferences.java index de5695340..47eebdc72 100644 --- a/src/org/fdroid/fdroid/Preferences.java +++ b/src/org/fdroid/fdroid/Preferences.java @@ -49,7 +49,7 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi private List compactLayoutListeners = new ArrayList(); private List filterAppsRequiringRootListeners = new ArrayList(); - private List updateHistoryListeners = new ArrayList(); + private List updateHistoryListeners = new ArrayList(); private boolean isInitialized(String key) { return initialized.containsKey(key) && initialized.get(key); @@ -138,13 +138,13 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi } } - public void registerUpdateHistoryListener(ChangeListener listener) { - updateHistoryListeners.add(listener); - } + public void registerUpdateHistoryListener(ChangeListener listener) { + updateHistoryListeners.add(listener); + } - public void unregisterUpdateHistoryListener(ChangeListener listener) { - updateHistoryListeners.remove(listener); - } + public void unregisterUpdateHistoryListener(ChangeListener listener) { + updateHistoryListeners.remove(listener); + } public static interface ChangeListener { public void onPreferenceChange(); From 9b6bb724d6b1f0f99eef166fe9a75d6542785ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 9 Feb 2014 16:37:39 +0100 Subject: [PATCH 065/282] Run remove-unused-trans --- res/values-bg/strings.xml | 1 - res/values-ca/strings.xml | 1 - res/values-de/strings.xml | 1 - res/values-el/strings.xml | 1 - res/values-es/strings.xml | 1 - res/values-eu/strings.xml | 1 - res/values-fa/strings.xml | 1 - res/values-fi/strings.xml | 1 - res/values-fr/strings.xml | 1 - res/values-gl/strings.xml | 1 - res/values-it/strings.xml | 1 - res/values-ko/strings.xml | 1 - res/values-nb/strings.xml | 1 - res/values-nl/strings.xml | 1 - res/values-pl/strings.xml | 1 - res/values-pt-rBR/strings.xml | 1 - res/values-ru/strings.xml | 1 - res/values-sl/strings.xml | 1 - res/values-sr/strings.xml | 1 - res/values-sv/strings.xml | 1 - res/values-tr/strings.xml | 1 - res/values-ug/strings.xml | 1 - res/values-uk/strings.xml | 1 - res/values-zh-rCN/strings.xml | 1 - 24 files changed, 24 deletions(-) diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml index bb8ac1dd4..de283c920 100644 --- a/res/values-bg/strings.xml +++ b/res/values-bg/strings.xml @@ -73,7 +73,6 @@ Дисплей Експерт Търсене на приложения - Вид на синхронизация на базата данни Съвместимост на приложенията Root достъп Игнорирай сензорния екран diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index dd14d6c06..eed37b7a4 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -84,7 +84,6 @@ La voleu actualitzar?
Pantalla Usuari expert Cerca aplicacions - Mode de sincronització de la base de dades Compatibilitat de les aplicacions Versions incompatibles Root diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index e184664e1..4b17427bf 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -93,7 +93,6 @@ Sollen diese aktualisiert werden?
Anzeige Experte Anwendungen suchen - Datenbanksynchronisierungsart Kompatibilität der Anwendung Inkompatible Versionen Root diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index b7eef575a..6a2b5e8f7 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -82,7 +82,6 @@ Εμφάνιση Για Προχωρημένους Αναζήτηση εφαρμογών - Λειτουργία συγχρονισμόυ της βάσης δεδομένων Συμβατότητα εφαμοργής Μη συμβατές εκδόσεις Root diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 996cdf9b9..ea3c51d98 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -82,7 +82,6 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Mostrar Experto Buscar aplicaciones - Modo síncrono de base de datos Compatibilidad de aplicaciones Versiones incompatibles Root diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index b4680d751..f37301049 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -62,7 +62,6 @@ Eguneratu nahi dituzu?
Bistaratu Aditua Bilatu aplikazioak - Datu-base modu sinkronoa Aplikazioen bateragarritasuna Root Ezikusi egin ukipen-pantailari diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml index 1d4c28915..9a6fa1356 100644 --- a/res/values-fa/strings.xml +++ b/res/values-fa/strings.xml @@ -82,7 +82,6 @@ نمایش خارج‌سازی جستجوی برنامه‌ها - حالت هماهنگی پایگاه داده‌ها هماهنگی برنامه نسخه‌های غیرهماهنگ روت diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml index 367475645..26c27f90d 100644 --- a/res/values-fi/strings.xml +++ b/res/values-fi/strings.xml @@ -61,7 +61,6 @@ Tahdotko päivittää ne?
Näyttö Asiantuntija Etsi sovelluksia - Tietokannan synkronointi-tila Sovellusten yhteensopivuus Yhteensopimattomat versiot Root diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 55e0d974a..51677523d 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -82,7 +82,6 @@ Voulez-vous les mettre à jour ?
Affichage Expert Rechercher des applications - Mode de synchronisation à la base de données Compatibilité de l\'application Versions incompatibles Root diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml index 3b28894da..0175f1829 100644 --- a/res/values-gl/strings.xml +++ b/res/values-gl/strings.xml @@ -86,7 +86,6 @@ Quere actualizalos?
Amosar Experto Buscar aplicativos - Modo de sincronización da base de datos Compatibilidade de aplicativos Versións incompatíbeis Root diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index fde3ceaff..6ce7c1d92 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -82,7 +82,6 @@ Vuoi aggiornarlo?
Mostra Esperto Ricerca applicazioni - Modalità di sincronizzazione database Compatibilità applicazioni Versioni incompatibili Amministratore diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index 28be1501c..5baeb291d 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -70,7 +70,6 @@ 표시 전문가 응용 프로그램 검색 - 데이터베이스 동기화 모드 응용 프로그램 호환성 호환되지 않는 버전 터치스크린 무시 diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index bd501e332..02159393f 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -77,7 +77,6 @@ Lisensiert GNU GPLv3.
Vis Ekspert Søk i programliste - Modus for databasesynkronisering Programstøtte Ukompatible versjoner Rot diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index 6eeb3910a..2d3539b10 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -85,7 +85,6 @@ Wilt u ze vernieuwen?
Laat zien Expert Zoek applicaties - Database sync-modus Applicatie verenigbaarheid Onverenigbare versies Root diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 383525464..155d056e7 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -78,7 +78,6 @@ Czy chcesz je zaktualizować?
Ta aplikacja wymaga innych, niewolnych aplikacji Ekspert Wyszukaj aplikacje - Tryb synchronizacji bazy danych Kompatybilność aplikacji Root Wszystkie diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index c51a79245..6c3b972bc 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -82,7 +82,6 @@ Você deseja atualizá-los?
Exibição Especialista Pesquisar aplicativos - Modo de sincronia do banco de dados Compatibilidade de aplicativo Versões incompatíveis Root diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 225e618e9..ccb7394b6 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -70,7 +70,6 @@ Вид Эксперт Найти приложения - Режим синхронизации базы Совместимость приложений Суперпользователь Игнорировать Тачскрин diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index 304697388..8f98a56bb 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -52,7 +52,6 @@ Ga želite posodobiti?
Tämä ohjelma sisältää mainontaa Napredno Iskanje aplikacij - Način sinhronizacije baze podatkov Združljivost aplikacij Yhteensopimattomat versiot Skrbnik diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index bb7a4516e..fad6b6304 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -81,7 +81,6 @@ Прикажи Стручни Претрага апликација - Режим синхронизације базе података Компатибилност апликације Рут Игнориши Додирни Екран diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index 49eda1b32..58cc6cdec 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -92,7 +92,6 @@ Vill du uppdatera dem?
Visning Expert Sök program - Databassynkroniseringsläge Programkompatibilitet Inkompatibla versioner Root diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml index ab4e6a6c8..28aaf249d 100644 --- a/res/values-tr/strings.xml +++ b/res/values-tr/strings.xml @@ -82,7 +82,6 @@ Güncellemek ister misiniz?
Görüntüleme Uzman Uygulama ara - Veritabanı eşleşme modu Uygulama uyumu Uyumsuz sürümler Root diff --git a/res/values-ug/strings.xml b/res/values-ug/strings.xml index ffb278803..53c8c774b 100644 --- a/res/values-ug/strings.xml +++ b/res/values-ug/strings.xml @@ -82,7 +82,6 @@ كۆرسەت ئالىي ئەپ ئىزدە - ساندان قەدەمداش ھالەت ئەپ ماسلىشىشچانلىقى ماسلاشمايدىغان نەشرىلىرى Root diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index 4ea3a11c4..d9af9a175 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -53,7 +53,6 @@ Звантаження скасовано Експерт Пошук програм - Синхронізація БД Сумісність Суперкористувач Ігнорувати тачскрін diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index 80451bdda..b12da2de2 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -53,7 +53,6 @@ 下载取消 高级 搜索应用 - 数据同步模式 应用兼容性 Root 忽略需要触屏的应用 From f525af993f841dca2a64084ea9d0c5699b0ab566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 9 Feb 2014 16:40:38 +0100 Subject: [PATCH 066/282] Run optipng -o4 on all png files --- res/drawable-hdpi/ic_repo_app_default.png | Bin 2219 -> 1124 bytes res/drawable-ldpi/ic_repo_app_default.png | Bin 1104 -> 665 bytes res/drawable-mdpi/ic_repo_app_default.png | Bin 1441 -> 872 bytes res/drawable-xhdpi/ic_repo_app_default.png | Bin 2118 -> 1397 bytes res/drawable-xxhdpi/ic_repo_app_default.png | Bin 3304 -> 1981 bytes 5 files changed, 0 insertions(+), 0 deletions(-) diff --git a/res/drawable-hdpi/ic_repo_app_default.png b/res/drawable-hdpi/ic_repo_app_default.png index 6efe7764a54d87d283b7c2a05d8ad7a5fb1e5694..798531939f4397c2e629d5551a66aecdf0111f5d 100644 GIT binary patch delta 980 zcmZ22_=IDEq&PDJ14ELrb19HwE_U(^;o#u7{m}oxM8nQ{hLZt4A+A6PAZTc42n`KQ zOH1?g^h`)d0J433eSzGhq$D6aE-o%IGBP$cHYO$}0t|p6KpH3#6%`d89u6dd3V zNswPK10xeND?0}#7Zcg_Vu1gOgjmho_gfPi$O#d_rPU zMrLk)K~Zr@X+>pKb#+Z`U0p-tu}v$)85o!fJzX3_A`ZWu5gonQK%gyn%hs%kT8lEX z!vh8VLs#v1y{l=~|No227GCiYD%y0@djD#NXO)d7&!jLp-H!^5wLfxMlke|4t_g{A zM4tq^it+Df__#<#z3G!m{iihsvVS~Os*cDOpF3z9_~PFI0pS?o-gV5v=U&Xs_-c2_ z+EUTY%f#>g4#l=zHvk)T?tx7>)*sf%aW7&wz#p(KzuIBQba^?Gp+hNoxh4zk_#+uJw!=39w1p6NB#v#(sY zUSoPEI;recjI8_fHKjbujP&;Mht4@He$9%xxOKuzh!X6bC%tnGFPMY>xz`O%ilRxm4y0k-e#RRprsv48033>!B{SLu@K0fT)-0^*XzrZ^_(|3JFk?;8G-t{fw<3Ikc?=@e& n!j9idAAV5pzI@;Ae$)$nb}3P#b@$IofpVv(tDnm{r-UW|`ud>T delta 2084 zcmV+<2;2AM2&)m085jlt0033(vqt~`00eVFNmK|32nc)#WQdV4iGK&%Nkl-r86Jw?Qwt*x!?rKP3k z?P}v|b&xE}gNP_1;%NXlj))OAazs=IfC>?<645mz9yc0|uND>-?uV%~GzU#gOq|el{Zq!+$*^?#fpfkg z2*Q_hb90Zw&?khUM3&{Vh$tiC@i6oS0TKHF;EW_m&ooU7JeUXSLB=5bTSWXQXhT{7 z5v|1I@oPZ_aev2%n7cc%)C=_=62>bfP(&_X-Bog^OA`beI zf6eqW#y${5alKqFdv=m%2c^^L9>X~UUS-<_BK9!G;-V-P%jI&zi!Ad}SWy%fjYcon zIA@P@zT-t_m=Mu(0C-{c8zK%wqtOeBqIj~|Qx6&(9Dht9;*iD94`Z>|A9i+jZtJ=} z__})-5#8V3-oBhjB$hWfH{S<r0v;x`vBUVM1x z&Yi~S=;(UAULOX4Znyda^0c?NcV>QmzP7x)Ovz-j7c3?ZMEsc~Njq+r0InP~F)?uh z5oNIXB!7wMs3?kmEtku5`}Xa+BuS4EaRd=ty5A!r;+)T4xpL*-008N9T40Q&5V6-v zhB5YwWHPx{EEXMygDy9XFjmO6&P2qwjV;DC003209{_;jMt_)eb#XCV9^Jt;NMf(+R;V zcD9-h(y}ZM3WD%!uN5I88yOiX7mLMb`=4u?_IF8=Y5))cfF}TOBcIPNyVYANm4DWU zhK8O2K!k{@obwy1sy4|_PEP(506zB7?jw>Ut!bLp^ukQ1c|`QxdpPI61c0Si9+Rr7 zD*&+KQ!VB5`I`W6(?#9BFl=txewcn@#<`U*i0Di!s>C-n_dE&VrMN!!5>S_W2K4*+Y{pvAnw|5W` z1$yg@v9IUm=DrT1!z;#%C}y+SBLMI_zj_SY|Fi#D(;$tAfxczNSSg5n5P$0RdZ4<^ zY;SVV_bDiTAfm?1%uL0vd{7F7!Y&bc3d--t%yv%!?88WtgLGYQ+o?pmF|)nNK}7WQ z^)%<;VP?D8Abn_ikeTgG4%*tw$cF+u zTkWcu;_>(cco|^-vGc^R)fBsvRn9rT-jXIAAUNmO?E-tYyLltMKa_;* z1IlB=MoXN+9n@$vzH(a|B6j6+xwa*f=W@B@X2;Z47`EA-UkyP_x-1wj~u*564D%0|6jpYh`yfB@%W zg+gIB7K_aik>8x3mP14THT(lqT+ zGMQZCoDUdxM%xDGe1kDo$mjEGVd(2z3wNON#{v$tY|PL!?Yr~m&)=+6Dh~nRIT0O0 z#K4(g054<60B{!o{t}PJ|9t7vrGJH?*No1en>f( diff --git a/res/drawable-ldpi/ic_repo_app_default.png b/res/drawable-ldpi/ic_repo_app_default.png index 6d3ce44ee571fd980f4918b417e0c3556ff0a95e..47f30b576b1504905fe5896651e30acc6d0521b3 100644 GIT binary patch delta 517 zcmcb>F_U$Iq&PDJ1H-j@8LNO4bFq_W2nPqp?T7vkCK`6uGn5ASgt!7FfS{qF!NI{H zEiElHG}O<}4@kzv#YIO)M?^$KMMVMGF)=ZbFaQ(*GT|gp8ITK92@wQ}!vRDJ$OTFP z*${Cc!&oaqvL0xydP$I9FaskK3o9Ehzp#v~s+zinwxylDqmzq|uU~L>8AmDnNwUnWEQeg6((o6sUcP5*??K^mA(;j_I zz2zS38EV{HCwOJ6x?b$M@~g)&JvMz}QM=FXPct)l)Qo)Vn|RD-TueK#K>Bm}*?>3e zW*h$BdExu?Q?s5NyL4=G+f0>N2TvXQ+Hvo>Dboy}J&EThp8pspU(e`}dgh8$aIzM| z`(srvIXsUnHJ#RBB`QAk7d@|`mDPHs_!cbev15(dFZd}hSusShOZCLdm$0~ zQbBb8m1Nr=yxSHPJ~og{mR>T^T_e}B`AW9O#skp`C%rB(afY3-5$XDw;mvm`v$;6n zth?&U%M<#$Pm+=ewfh?Nz_VrVzLeV!dE8(EgD$RsI)e!*bt`Sr2hA9`K^uQgsY zNl?9BKW!MsJQ2+>bAL%GRRREJK2S;>h{zMIb$4T9=}l!!EYPEjP^PS3bFimq=DRrHQ-kt;o0Ep-; z5q-R{uppgI=kZwIEW^smicv0?uPLQ2P70DHX5I~gV6)wB>wh%6$_k9bhzJ9CaTGvG zsf*=u`C67eb;zP&`mFz3M7|@UyUhF%5oP{~G%@oF08J73lZZY903v!XYxqWhq6vJR zhz7$cB7ZfT&0iK57ykh86u@rHHn9KeBYA)kKMF zpVn%%w&!^d0DnBNZTlSna9#Il+FeQ7YqeSlz@@kk5s^~rDu7!40a(k~3doqw49empiqb8-7%u9&7-RZ88+ ziV~4Kl}hEF<2XDf`G_3HQNHhgPDEF-e3VF^1p?(XIrRnME zpNhf&K!3a4)>`X(<9y<^554QIudjziVfrf47Y%@zId!i%3B_y2hwPOB#cL0MMC8e< z!5OYS0Mc4_qqg&;VYJq|I|39W8#piYxcz+Uay2SF6=WMD#8Zot+S7pP6sBTCLnW{{MX0 ikR8-EBE7z5=10000Q!UeJ;BO@cgATBNrNJm9Q#l*w_1%V(uJRGPJC;|jPDWC{Y3Mc|m4-|yR0>yz0 zkOnY-2tvtvAPqJGLc;ZcjEsr`s)v(62C|x2&Q|AvuC^`-@(X5QX5-@K;};SWmy(rJ zP*vB^*3mUEHnFs_wsmlHc5!v{@C^tI3JDF5N=Qo1%+1R$EGj9js;#SUXsmZhFsx@_ zV4Ujd;uzv_{OZ)`>Lvr8w&uf|XXnlPXks>li_fw3*EzZT{kumjGW&Dw{0_Anp+rS*2v_QN`!^X*PI zOka}DnxMeU#`NHiNI{GHbcSgYt_d9w;RO4IePB89ZJ6T-G@yGywn$!6IG& delta 1299 zcmV+u1?>9h2B8a(85jlt00374`G)`i00eVFNmK|32nc)#WQdV4iGKvqNkl z54mDbfGXN?8 zj&>8v%p1(SMnua&5Zv#TLr(%6#~HCKYm%AA0rd3|n3(~rgkiYYXf!svjqj?s<2XjC zRGI|v8Gur^#X5nRV`g58<9M;rXh_{SZ2`XT4=JUlh-kDYhJX547eZXF)oR;1&W4VY z>$=C8`D+00^v6vLN0d^hY}>xqY&Hu;6bf)%_c(y_0Lp{mR}27e5YcJdw(k~+5S@&^ z?+*d+036zR$IOqJd4-wPfcR39>$Xzr<)0Y9ApoB5`$L^#a|Jk#V<@Gj032z358%ea z!orWte64Th34fUR+Wh?d4?>9Rsai*rQd5rONal~OBtzO6Q9j?3lpoo;GnacdLc`~ESVU8NA>y`A6HYV{d_3jnT4DKFOR z^;>yy^?Lo5l=32gs{k%ktJP;&IXehA>ihn&ws)-$)6>&m7>4m>Q6i<(Q`0mr&&|#K zRphIK+1c3-qbQmZLKLp{Qpy|kdj02}=eF4-Ie&~4LI_coQl58RcT)%vbx-+UD5Xp( z<%kd+HwC#I(^KVP&AO6^mYDgjl-fJrJVK^Y>J)%+06Hq#IJOA@FsxHlDYYB~!N9f> zMD&N}c_tCrMUH8XZHjl!up0PNpPL?}lv*=Vnxo#p%c$35TrPwVaj{oMT{6Ob?-bFCfh^J%pa@A(vj!#<5L+QtIK}a_Jsvj%@-=(_Eu{jx@(M0kvA~0W)vx zEhT+0^G3QQxoa9t+I{;HS=_SLfn{dqJ>b(75>CtM_qzlHL2w^H?w$MK?NSszeFZwSD!VHn?fp7%Eq^>rQ~A``${9qj9I9A|!gqJr+U@B2ei%5T!$ zxxE52KQ;`b+No3BvAMgtx*FNGeUFGv5Bff!2+aJPndj!`=O14q46@WoR zCp6Em+DNXLeu<*!ryvNP>&E@RF8R{<&t9`;;dNbfAK(Ce;vd!BDEucZWEKDb002ov JPDHLkV1mmDc~$@b diff --git a/res/drawable-xhdpi/ic_repo_app_default.png b/res/drawable-xhdpi/ic_repo_app_default.png index 0447333e68f423067cacffd1f8aaf50c1fbaa100..b224d0a2e62b5258e8a622235d5a3d68d57a3f79 100644 GIT binary patch literal 1397 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy#>i=8|}I5;?NKlFb92xL>4nJ zXtsecqtcuEJwQRp64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq=1U#L5e~$ zOL9^fQc}|rOLIyT5(^4)GLsWaGV}9_6*80a^UU772nQ+^11WVZEy>6)VhGAlN-Zi; z@J%c#$;?Zw+V*RSAyCEJ0G|+7ARRSgz?OMTD}f&NFA4GsW?*DuW@TgN;N;@w6%dh- zlu}SsQdUvZ(A3d0Hn*^}vbM3cvv+WEc6Imm@eK?P2@MO6h>VJkjf+oAO3lg3FDxo9 zDJ`q0tg5c5t8ZvDRJCCNMs$#;i(^Q}y|Z`I#ze{$wbYnsveBT3b71Vt~cT6TR_*!^w4!!|F=&Z?Sz zt$3>3r-k7s-yW&3$?p?jRyLNOZc-mLf8p90SN&Db$Vcj{aMfA8qLig&p$tZmG#8zY2x!2otNIW z_PgTO(*0KFb}=cgu$VM=z4Z2{e%kYXe3(}b~ZGxN)lMUh1Fy`Ue<|wjMA&-Sfxw$@G=KbE6j%VMe{L5YQ?R@y^ha1CxRP4BA z8?P&!GH+QZQ>lx3!TJgBM6cOipXk)^YRa$lV+sY|-|xNjJ@4uvg&99zFSSV$*s(9W zYN_vaWl0u|_sRy5Eak6L4{FCo_*m1j-f|5sWyyVi9zJRfT(VN4v8{|x*KPMNi)U}=*M$7PWNa=Lm1H{Wgha&o zRl>8%cqeCGa@ps&_l33Z?#|AL8u#06c55Xbcx`#eBHL9ZKY78=ueEIA z+MhE~`9vePzG0K#o)QsdgNH}gOE}8K?c){x`&-+1t-a^Nq%pbZmeX}sYn(a>m9Ek7XhEyDQE!JHq9pI&Hx{ILJKa>W9U|DCFT z+!uYAzw=Q2itYz14V#W%;+R*hT(P0=$;sM&+ij2Rv-=W?(tVD#05dLwr>mdKI;Vst E01j(aeE z%Zpw`7QlZdlb*COu@QBQK_?874-YW@u+2xNu`IXBZI#_kw?h;8p}TDhKfa znT-pXAU%$8lZ8+n64Z>X<0~d=Ogf2nFzuqMrR{w8QFW{Cce}s)gKyFOeRa;c=iFQG zIv09+dU|?#dU{5Y%YTM#Kp7%30Zaf}fGdE_13my}27Km$)4(ZJJ=Lz|_32yyMC5v4 zhyPrYNbCvV2yjGIzf2@E2}2Knh^zx9aSp!&cem%{G;kCbkU3Som|0&bLkob2YzC%) zoxu2z%s3Cs0yC<5W*7#v9sm*fJ#as;2iVZI{sJxl2Z8rg^?#=}6}J`u5m^uH!rk?o z+R_`ySzrb@q^du+p)h*@MC3ByE?^qCD!0Bwd7PDNP*AR?Q9y|_kpH8Kah zrmAOiDOjINu!vj-JONynOGgV#0CyQ<=B%~zITU0GfQak>{y}%^`Vck(cN=3)T5C_H z7M?r+A~FR$41fGCwd^6X9`__HT5CT|DL7dhAR-%pM{xc6wahHAUsW%p5SHW#@Xmh% z_)`jb9p_qLyD`REYa5TE*Cj9r+?{_zB3Y|}8*ta$n8r_luk-(sz?Rj zorkGzOAY}4!p%xf(%lAn0ren$!quKyJ!re!;^8tP{x#chL-59-@d3U+< zJ#{AL#(;g^C&2^*JYnGn?_Ilq05nMn- zb^?zE7JvH?(B%_b{*JjZRDUM^zkI&OJ5qq#0_D7}sYRZn*V@X4}U(Us%72AbKch2n`KHwKB&90R(l*^ zjF|#%jWi0}g%{S3BC;EJ47eS*3Ahv3X^ik>0Q|n4+*8cgSA4%U zLS|K&DxFQ)>3vs9ohM+D{QGv!SA73tgv_ckRXUq8PK~wjIsw?x*pzDiSMmMIDS|pw zm8sI1f=b`j3BVl*45{J(ZZqv5W>qzKz<=h;6!p61z0TZJ`mQtpexq!vy_#1PeW9x7 zfaih3xYW6lE4R&9p$ z^DrA#HEA6!BICf1RU55*!}$#9c69tlRSTcCs*!NY`hOFy>o-&OBk!Q(4P-z-AAcb2 zcA6cORCk3>fIl4ZbV5S_F5=b#X4=|&SrU;oIox^RPNlhmL za6(l-PAoGys`?@DZDO&V2*5Xq<>lmfVtJhiK$6|WEzxKhbO2ES=xKqn01V~a>>8!e zmwf=*p%1{2=>sri`Tz`>r~qU+7dxU9`m!tlr!yEbsucRNEC8L&GPMdR^nYbx0Q?Hh zi7e)f7AMM5rd5rEk0dr`6e;y#AAtPC2f!z|>l@>EPFE!ftTP$c)ni1fflA-i2|!;B z#Z&3KIsurYJKINA6X&UM6t^+}ek){Im`KlJR1K`iS)_SZ8SeY-P%QUfdCM5fA=%V}~ zTvF2*w#Fp0Xd7>1%%MPVzBv?9ASeKSOL^?kp3T7S$g*v+oA@m`2ST^q$7HTL4E&k6 zY`A~-ci8*6Qv!$L?Ya+mFjB-E&No3sRS70jpQ7a#Jy5W&V@W?x|X zutO<<-au$*=usNOKgf&1@;=H8q!ulkD+wU@L;;7B;{Lb`a_m3+m+lX6Y4JE||upB}(u-3SZ{4hanlkBE$l zj=32dM<}!woR5gJy^W+5vnyK2IE2DMvllz~vy7nap5?xiK?8+&-^xOtdo=7Fa^GCLz?Zdtc`RgSX<)N7CC&ub;Xa=pW= zJ?%)3W6)oxe9@9;Kb;zvaugP64+=0do!ZN;zIR}<$C5MDY*MMgk92%ix6&?|z0ta# zFcihJjuoprg)P=@_?jPHb+RDr z{>K7ty8$`m+Hz-VKkCjCr#F8L+O`azJ8oWL+cHYMumWRt8llkc6}EA$BMN!Okna~R zy+5Wt59-*>J<^jk{6YM!&WN=GyTR0^8ngJ%H@xyL_tk^9bqOAusPsKm%0Y0sDBEWA z*^AK3q+88_(EgL=ldfg1uH)g#{l}G|eJaO)@YPGU_17|t^n?IYq&fzP0%CjnhbofP z^?JvDjM#}+5jP&0#H+}z7^OTYwt>DvV>Ful(xN1^{Qe;)75J%0`+HFS0r$lZfzgq9ajCPU1gp^fu68W6g zqeQfZUxlcLSAQUCQ~ zzRPfTSXj`0&yu-vI?;?*HeKuM)>Lt9P-RZVVPVQ4op;F{Td}#t{HuDoeX_xk`(i-x zJ=t0F$W{Hc5j;w)jd0eR(x=~Z(`5c#nap`MDvi{bh~VCr*>aHbO=Y}EKv;PYKwbA} zcP)n9`k)c0MY18vy2Mw-=hn@9+&&+;{t*+zhyO>8vvE7lv|Zu>r8MvPlD;Gl8uIjZ z++zDo5~Z}373-s@si)koB-#TJ+qxYmpV(;W6BqFbiLb*ER!RqSGBF&wzC_NUbKQy} zYUN&4109+~YqbihY*kg>8c!yNj6xrKn5WeJDp<>lTI6~96LIA=-@ZQzs6*f zXh`Lk2d6S;!(*AoUl_<@zbCt=VPX6GI4l0(%GX}pbitWfqqEM!H4n4JRYsg-B^znm#ttpET3 delta 3177 zcmV-v43_h~59k??85jlt008Gk; z-H%qq9mhWpuL=kvph)DUTeZIQAUCF|ut^otCSZ$PkeFXD+InG9(?6j73)(-RX|FW( z!bu}l6BETYN*fU31#OX|DK!==FN+FveW7fDUA7l94?M2>GIP$%nK{qSCpp=R-DiHk z^ZdR$@ADhTWHOmdCV!L3WHOmdCX>lzGMO3Z#C9MCB60(;0$62!t^jTX7W6y|%};>K zz$Npu`-}kR&2ttwuax>ypnFrhf3;^rUqFEBi+y%6lP5{S&Q%b4PK(>;g z?*t?w9bgSwFay9^V16Wbt^g;|Vmb~CDW%3E`4WV_5s-+i1b;S~XL)3IreYX43LI5R zor~>^4_YlC5m^irzys)7r%en2Zv&-L>c6p_sY)vaBqGa!Ex;yVUSxObG7204-c(8r z$9Be*Rtd;7Q-kPcYEEQ#d~+4uyd7v$^VSLhiO34z5#WAW;&Tngfp>v@N~!a)otcTG zgCQdG(KXIy+JE0vxiShIMAt}HVmmVx$qGnB?gqA#?ue=>hJja=Qt!ofWHOQykccb= zb^!Op_9Zcn0k0^fK9B84FG&f=)H=I>MX`NJl#9SiDb+?v2uMWc0Na3vNW`av83zsl zuPddl#&#xputa1Tup7PPkg?|^utzENSxhJ9#xyP>Uw;FBj9v}TxUme_R20SOvMfK1 z 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 P00000NkvXXu0mjf*opsH From 0ff1257aef1f6fa3e1a565d3f646d5c39fb09bf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 10 Feb 2014 09:15:03 +0100 Subject: [PATCH 067/282] Update submodules --- extern/Universal-Image-Loader | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/Universal-Image-Loader b/extern/Universal-Image-Loader index 45e5f6768..f49a5a0e5 160000 --- a/extern/Universal-Image-Loader +++ b/extern/Universal-Image-Loader @@ -1 +1 @@ -Subproject commit 45e5f67683430d55b5a6ea65130307d6b45ded67 +Subproject commit f49a5a0e50d5b817c1c531abed3b7945f8a7ff42 From 5292acfef0b843d357bf12554837c685b48ee150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 10 Feb 2014 09:22:43 +0100 Subject: [PATCH 068/282] Add support for arrays in remove-unused-trans --- tools/remove-unused-trans.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/remove-unused-trans.sh b/tools/remove-unused-trans.sh index 7c6e49007..574c2d2ab 100755 --- a/tools/remove-unused-trans.sh +++ b/tools/remove-unused-trans.sh @@ -3,7 +3,11 @@ # Remove extra translations lint . --quiet --check ExtraTranslation --nolines | \ - sed -n 's/.*Error: "\([^"]*\)" is translated here but not found in default locale.*/\1/p' | \ - while read name; do - sed -i "/name=\"$name\"/d" res/values-*/strings.xml + sed -n 's@res/values-[^/]\+/\([^\.]\+\)\.xml:.*Error: "\([^"]*\)" is translated here but not found in default locale.*@\1 \2@p' | \ + while read file name; do + if [[ $file == strings ]]; then + sed -i "/name=\"$name\"/d" res/values-*/strings.xml + elif [[ $file == array ]]; then + sed -i "/ Date: Mon, 10 Feb 2014 09:23:40 +0100 Subject: [PATCH 069/282] Run remove-unused-trans again --- res/values-ar/array.xml | 5 ----- res/values-bg/array.xml | 5 ----- res/values-ca/array.xml | 5 ----- res/values-de/array.xml | 5 ----- res/values-el/array.xml | 5 ----- res/values-eo/array.xml | 5 ----- res/values-es/array.xml | 5 ----- res/values-eu/array.xml | 5 ----- res/values-fa/array.xml | 5 ----- res/values-fi/array.xml | 5 ----- res/values-fr/array.xml | 5 ----- res/values-gl/array.xml | 5 ----- res/values-gu/array.xml | 5 ----- res/values-it/array.xml | 5 ----- res/values-ko/array.xml | 5 ----- res/values-nb/array.xml | 5 ----- res/values-nl/array.xml | 5 ----- res/values-pl/array.xml | 5 ----- res/values-pt-rBR/array.xml | 5 ----- res/values-ro/array.xml | 5 ----- res/values-ru/array.xml | 5 ----- res/values-sl/array.xml | 5 ----- res/values-sr/array.xml | 5 ----- res/values-sv/array.xml | 5 ----- res/values-tr/array.xml | 5 ----- res/values-ug/array.xml | 5 ----- res/values-uk/array.xml | 5 ----- res/values-zh-rCN/array.xml | 5 ----- 28 files changed, 140 deletions(-) diff --git a/res/values-ar/array.xml b/res/values-ar/array.xml index 6b1fbe25d..f26ac258c 100644 --- a/res/values-ar/array.xml +++ b/res/values-ar/array.xml @@ -11,9 +11,4 @@ غامق فاتح - - معطل (غير آمن) - عادي - مكتمل - diff --git a/res/values-bg/array.xml b/res/values-bg/array.xml index d7738bcf9..e81f9ad28 100644 --- a/res/values-bg/array.xml +++ b/res/values-bg/array.xml @@ -11,9 +11,4 @@ Dark Light - - Изключено (опасно) - Нормално - Пълно - diff --git a/res/values-ca/array.xml b/res/values-ca/array.xml index 4ea57069f..01d48006c 100644 --- a/res/values-ca/array.xml +++ b/res/values-ca/array.xml @@ -11,9 +11,4 @@ Fosc Clar - - Desactivat (no segur) - Normal - Complet - diff --git a/res/values-de/array.xml b/res/values-de/array.xml index cd7204197..722843fd8 100644 --- a/res/values-de/array.xml +++ b/res/values-de/array.xml @@ -11,9 +11,4 @@ Dunkel Hell - - Aus (unsicher) - Normal - Vollständig - diff --git a/res/values-el/array.xml b/res/values-el/array.xml index 27b24978d..2f02e1184 100644 --- a/res/values-el/array.xml +++ b/res/values-el/array.xml @@ -11,9 +11,4 @@ Σκοτεινό Φωτεινό - - Απενεργοποίηση (επισφαλής) - Κανονικό - Ολόκληρο - diff --git a/res/values-eo/array.xml b/res/values-eo/array.xml index fface9219..79d432e35 100644 --- a/res/values-eo/array.xml +++ b/res/values-eo/array.xml @@ -11,9 +11,4 @@ Dark Light - - Off (unsafe) - Normal - Full - diff --git a/res/values-es/array.xml b/res/values-es/array.xml index ce0b75b74..49d24bead 100644 --- a/res/values-es/array.xml +++ b/res/values-es/array.xml @@ -11,9 +11,4 @@ Oscuro Claro - - Desactivado (peligroso) - Normal - Completo - diff --git a/res/values-eu/array.xml b/res/values-eu/array.xml index 80e6b4251..975c0749d 100644 --- a/res/values-eu/array.xml +++ b/res/values-eu/array.xml @@ -11,9 +11,4 @@ Dark Light - - Itzalita (ez da segurua) - Normala - Osoa - diff --git a/res/values-fa/array.xml b/res/values-fa/array.xml index 14d3a0421..f9d446d9a 100644 --- a/res/values-fa/array.xml +++ b/res/values-fa/array.xml @@ -11,9 +11,4 @@ تاریک روشن - - خاموش (ناامن) - عادی - کامل - diff --git a/res/values-fi/array.xml b/res/values-fi/array.xml index d4ef0d648..24139afd6 100644 --- a/res/values-fi/array.xml +++ b/res/values-fi/array.xml @@ -11,9 +11,4 @@ Tumma Valo - - Pois päältä (vaarallinen) - Normaali - Täysi - diff --git a/res/values-fr/array.xml b/res/values-fr/array.xml index d204ebad8..61b0d885a 100644 --- a/res/values-fr/array.xml +++ b/res/values-fr/array.xml @@ -11,9 +11,4 @@ Sombre Clair - - Désactivé (non recommandé) - Normal - Complet - diff --git a/res/values-gl/array.xml b/res/values-gl/array.xml index 1770571ed..722add613 100644 --- a/res/values-gl/array.xml +++ b/res/values-gl/array.xml @@ -11,9 +11,4 @@ Escuro Claro - - Apagado (inseguro) - Normal - Completo - diff --git a/res/values-gu/array.xml b/res/values-gu/array.xml index 42eac981d..da5e2aa16 100644 --- a/res/values-gu/array.xml +++ b/res/values-gu/array.xml @@ -11,9 +11,4 @@ Dark Light - - બંધ (અસુરક્ષિત) - સામાન્ય - પૂર્ણ - diff --git a/res/values-it/array.xml b/res/values-it/array.xml index 7c2f1ef01..2eb94a3d6 100644 --- a/res/values-it/array.xml +++ b/res/values-it/array.xml @@ -11,9 +11,4 @@ Scuro Chiaro - - Disabilitato (non sicuro) - Normale - Completo - diff --git a/res/values-ko/array.xml b/res/values-ko/array.xml index 2bf47cb19..d00633ed0 100644 --- a/res/values-ko/array.xml +++ b/res/values-ko/array.xml @@ -11,9 +11,4 @@ 어두운 밝은 - - 해제 (안전하지 않음) - 보통 - 전체 - diff --git a/res/values-nb/array.xml b/res/values-nb/array.xml index 2fb6485b1..bdf6445f2 100644 --- a/res/values-nb/array.xml +++ b/res/values-nb/array.xml @@ -11,9 +11,4 @@ Mørk Lys - - Av (utrygt) - Normalt - Fullt - diff --git a/res/values-nl/array.xml b/res/values-nl/array.xml index 19c7f0690..765ae052b 100644 --- a/res/values-nl/array.xml +++ b/res/values-nl/array.xml @@ -11,9 +11,4 @@ Donker Licht - - Uit (onveilig) - Normaal - Vol - diff --git a/res/values-pl/array.xml b/res/values-pl/array.xml index 103711e39..e6076d5e5 100644 --- a/res/values-pl/array.xml +++ b/res/values-pl/array.xml @@ -11,9 +11,4 @@ Ciemny Jasny - - Wyłączone (niebezpieczne) - Normalny - Pełny - diff --git a/res/values-pt-rBR/array.xml b/res/values-pt-rBR/array.xml index 6fc95972c..1614797d3 100644 --- a/res/values-pt-rBR/array.xml +++ b/res/values-pt-rBR/array.xml @@ -11,9 +11,4 @@ Escuro Claro - - Desligada (inseguro) - Normal - Completa - diff --git a/res/values-ro/array.xml b/res/values-ro/array.xml index d8e58ad04..8c1fabb4e 100644 --- a/res/values-ro/array.xml +++ b/res/values-ro/array.xml @@ -11,9 +11,4 @@ Dark Light - - Inchis (nerecomandat) - Normal - Complet - diff --git a/res/values-ru/array.xml b/res/values-ru/array.xml index 13a7defd0..8b39f1618 100644 --- a/res/values-ru/array.xml +++ b/res/values-ru/array.xml @@ -11,9 +11,4 @@ Dark Light - - Откл. (опасно) - Обычный - Полный - diff --git a/res/values-sl/array.xml b/res/values-sl/array.xml index ba22674f8..bc3404374 100644 --- a/res/values-sl/array.xml +++ b/res/values-sl/array.xml @@ -11,9 +11,4 @@ Dark Light - - Izključeno (ni varno) - Običajno - Polno - diff --git a/res/values-sr/array.xml b/res/values-sr/array.xml index 0a0c851a6..7d414c084 100644 --- a/res/values-sr/array.xml +++ b/res/values-sr/array.xml @@ -11,9 +11,4 @@ Dark Light - - Искључено (није безбедно) - Нормално - Пуно - diff --git a/res/values-sv/array.xml b/res/values-sv/array.xml index 9edfdb38c..12b70f0e9 100644 --- a/res/values-sv/array.xml +++ b/res/values-sv/array.xml @@ -11,9 +11,4 @@ Mörk Ljus - - Av (osäkert) - Normal - Fullständig - diff --git a/res/values-tr/array.xml b/res/values-tr/array.xml index b7840e71e..73ba4a61a 100644 --- a/res/values-tr/array.xml +++ b/res/values-tr/array.xml @@ -11,9 +11,4 @@ Koyu Açık - - Devre dışı (güvenli değildir) - Normal - Tümü - diff --git a/res/values-ug/array.xml b/res/values-ug/array.xml index 35b79432f..25e167824 100644 --- a/res/values-ug/array.xml +++ b/res/values-ug/array.xml @@ -11,9 +11,4 @@ قاراڭغۇ يورۇق - - تاقاق (بىخەتەر ئەمەس) - نورمال - تولۇق - diff --git a/res/values-uk/array.xml b/res/values-uk/array.xml index 0d43d54c8..83ea4a1e8 100644 --- a/res/values-uk/array.xml +++ b/res/values-uk/array.xml @@ -11,9 +11,4 @@ Dark Light - - Ніколи (небезпечно) - Типово - Повністю - diff --git a/res/values-zh-rCN/array.xml b/res/values-zh-rCN/array.xml index 120aefcf0..dd10ac48a 100644 --- a/res/values-zh-rCN/array.xml +++ b/res/values-zh-rCN/array.xml @@ -11,9 +11,4 @@ Dark Light - - 关闭(存在安全风险) - 正常 - 完整的 - From 6da00f84cb524188d27b285802e43b4ea7630151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 10 Feb 2014 09:30:08 +0100 Subject: [PATCH 070/282] Fix proguard use in gradle --- build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 5fcbf2203..731e88b8f 100644 --- a/build.gradle +++ b/build.gradle @@ -35,8 +35,7 @@ android { buildTypes { release { runProguard true - proguardFile 'proguard-project.txt' - proguardFile getDefaultProguardFile('proguard-android.txt') + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt' } } From 52dc6f89778e77f61c4f4c5956609bdb516bfbc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 10 Feb 2014 09:37:03 +0100 Subject: [PATCH 071/282] Update README with new translation info --- README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a40de5851..2f8893f6e 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ Direct download --------------- You can [download the application](https://f-droid.org/FDroid.apk) directly -from our site. +from our site or [browse it in the +repo](https://f-droid.org/app/org.fdroid.fdroid). Contributing @@ -39,13 +40,10 @@ and our [Forums](https://f-droid.org/forums/). Translating ----------- -The `locale` dir is automatically updated via the -[android2po](https://github.com/miracle2k/android2po) tool, and translations -are pulled from our Pootle translation server at -[f-droid.org/translate](https://f-droid.org/translate). You should only add or -remove strings in the `res/values/` dir, since all the `res/values-*` dirs are -also generated automatically. - +The `res/values-*` dirs are kept up to date automatically via [MediaWiki's +Translate Extension](http://www.mediawiki.org/wiki/Extension:Translate). See +[our translation page](https://f-droid.org/wiki/page/Special:Translate) if you +would like to contribute. License ------- From 9ec0a9060cb7ea79a181b92f27a6f0c0d07bfb2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 10 Feb 2014 11:13:44 +0100 Subject: [PATCH 072/282] Add .fdmeta for testing reasons --- .fdmeta | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .fdmeta diff --git a/.fdmeta b/.fdmeta new file mode 100644 index 000000000..3c439de9f --- /dev/null +++ b/.fdmeta @@ -0,0 +1,7 @@ +Web Site:https://f-droid.org +Source Code:https://gitorious.org/f-droid/fdroidclient +Issue Tracker:https://f-droid.org/repository/issues + +Donate:https://f-droid.org/about +FlattrID:343053 +Bitcoin:15u8aAPK4jJ5N8wpWJ5gutAyyeHtKX5i18 From e4d106a298ec972876e96384b752cd220660a84b Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 11 Feb 2014 08:20:42 +1100 Subject: [PATCH 073/282] Fixed incorrect size of category spinner. --- .../fragments/AvailableAppsFragment.java | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java index 3bb104b65..51360851a 100644 --- a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java @@ -42,10 +42,7 @@ public class AvailableAppsFragment extends AppListFragment implements return adapter; } - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - LinearLayout view = new LinearLayout(getActivity()); - view.setOrientation(LinearLayout.VERTICAL); + private Spinner createCategorySpinner() { final List categories = AppProvider.Helper.categories(getActivity()); @@ -53,7 +50,13 @@ public class AvailableAppsFragment extends AppListFragment implements // Giving it an ID lets the default save/restore state // functionality do its stuff. spinner.setId(R.id.categorySpinner); - spinner.setAdapter(new ArrayAdapter(getActivity(), android.R.layout.simple_list_item_1, categories)); + + ArrayAdapter adapter = new ArrayAdapter(getActivity(), + android.R.layout.simple_spinner_item, categories); + adapter.setDropDownViewResource( + android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int pos, long id) { @@ -68,10 +71,17 @@ public class AvailableAppsFragment extends AppListFragment implements getLoaderManager().restartLoader(0, null, AvailableAppsFragment.this); } }); - spinner.setPadding( 0, 0, 0, 0 ); + + return spinner; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + LinearLayout view = new LinearLayout(getActivity()); + view.setOrientation(LinearLayout.VERTICAL); view.addView( - spinner, + createCategorySpinner(), new ViewGroup.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); From ef784dffa8d34deed67d529ca1349f9d0af4d6fb Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 11 Feb 2014 09:02:03 +1100 Subject: [PATCH 074/282] Update categories list after app list update. --- .../fragments/AvailableAppsFragment.java | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java index 51360851a..37e651aaf 100644 --- a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid.views.fragments; +import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; @@ -51,12 +52,37 @@ public class AvailableAppsFragment extends AppListFragment implements // functionality do its stuff. spinner.setId(R.id.categorySpinner); - ArrayAdapter adapter = new ArrayAdapter(getActivity(), - android.R.layout.simple_spinner_item, categories); + final ArrayAdapter adapter = new ArrayAdapter( + getActivity(), android.R.layout.simple_spinner_item, categories); adapter.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); + getActivity().getContentResolver().registerContentObserver( + AppProvider.getContentUri(), false, + new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + // Wanted to just do this update here, but android tells + // me that "Only the original thread that created a view + // hierarchy can touch its views." + getActivity().runOnUiThread( new Runnable() { + @Override + public void run() { + adapter.clear(); + adapter.addAll(AppProvider.Helper.categories(getActivity())); + // adapter.notifyDataSetChanged(); + } + }); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + onChange(selfChange); + } + } + ); + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int pos, long id) { @@ -71,7 +97,6 @@ public class AvailableAppsFragment extends AppListFragment implements getLoaderManager().restartLoader(0, null, AvailableAppsFragment.this); } }); - return spinner; } From 7ca6db9555addc4ac3c2e6483940fcb52b970e3e Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 11 Feb 2014 10:12:29 +1100 Subject: [PATCH 075/282] Added ArrayAdapterCompat so addAll doesn't need to be wrapped in a guard condition. --- .../fdroid/compat/ArrayAdapterCompat.java | 22 +++++++ .../fragments/AvailableAppsFragment.java | 63 +++++++++++-------- 2 files changed, 59 insertions(+), 26 deletions(-) create mode 100644 src/org/fdroid/fdroid/compat/ArrayAdapterCompat.java diff --git a/src/org/fdroid/fdroid/compat/ArrayAdapterCompat.java b/src/org/fdroid/fdroid/compat/ArrayAdapterCompat.java new file mode 100644 index 000000000..befe7974e --- /dev/null +++ b/src/org/fdroid/fdroid/compat/ArrayAdapterCompat.java @@ -0,0 +1,22 @@ +package org.fdroid.fdroid.compat; + +import android.annotation.TargetApi; +import android.os.Build; +import android.widget.ArrayAdapter; + +import java.util.List; + +public class ArrayAdapterCompat { + + @TargetApi(11) + public static void addAll(ArrayAdapter adapter, List list) { + if (Build.VERSION.SDK_INT >= 11) { + adapter.addAll(list); + } else { + for (T category : list) { + adapter.add(category); + } + } + } + +} diff --git a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java index 37e651aaf..7fb0bae68 100644 --- a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java @@ -3,7 +3,9 @@ package org.fdroid.fdroid.views.fragments; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.os.Handler; import android.support.v4.app.LoaderManager; import android.util.Log; import android.view.LayoutInflater; @@ -12,6 +14,7 @@ import android.view.ViewGroup; import android.widget.*; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.compat.ArrayAdapterCompat; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.views.AppListAdapter; import org.fdroid.fdroid.views.AvailableAppListAdapter; @@ -43,6 +46,36 @@ public class AvailableAppsFragment extends AppListFragment implements return adapter; } + private class CategoryObserver extends ContentObserver { + + private ArrayAdapter adapter; + + public CategoryObserver(ArrayAdapter adapter) { + super(null); + this.adapter = adapter; + } + + @Override + public void onChange(boolean selfChange) { + // Wanted to just do this update here, but android tells + // me that "Only the original thread that created a view + // hierarchy can touch its views." + getActivity().runOnUiThread( new Runnable() { + @Override + public void run() { + adapter.clear(); + List catList = AppProvider.Helper.categories(getActivity()); + ArrayAdapterCompat.addAll(adapter, catList); + } + }); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + onChange(selfChange); + } + } + private Spinner createCategorySpinner() { final List categories = AppProvider.Helper.categories(getActivity()); @@ -52,36 +85,14 @@ public class AvailableAppsFragment extends AppListFragment implements // functionality do its stuff. spinner.setId(R.id.categorySpinner); - final ArrayAdapter adapter = new ArrayAdapter( - getActivity(), android.R.layout.simple_spinner_item, categories); + ArrayAdapter adapter = new ArrayAdapter( + getActivity(), android.R.layout.simple_spinner_item, categories); adapter.setDropDownViewResource( - android.R.layout.simple_spinner_dropdown_item); + android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); getActivity().getContentResolver().registerContentObserver( - AppProvider.getContentUri(), false, - new ContentObserver(null) { - @Override - public void onChange(boolean selfChange) { - // Wanted to just do this update here, but android tells - // me that "Only the original thread that created a view - // hierarchy can touch its views." - getActivity().runOnUiThread( new Runnable() { - @Override - public void run() { - adapter.clear(); - adapter.addAll(AppProvider.Helper.categories(getActivity())); - // adapter.notifyDataSetChanged(); - } - }); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - onChange(selfChange); - } - } - ); + AppProvider.getContentUri(), false, new CategoryObserver(adapter)); spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override From 1083f57ec18d13684d9f3b5b2c74bfb46f352233 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 11 Feb 2014 10:19:32 +1100 Subject: [PATCH 076/282] Case insensitive sort of app list, if sorting by name. --- src/org/fdroid/fdroid/data/AppProvider.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/org/fdroid/fdroid/data/AppProvider.java b/src/org/fdroid/fdroid/data/AppProvider.java index e09b2c5a7..31db1b37e 100644 --- a/src/org/fdroid/fdroid/data/AppProvider.java +++ b/src/org/fdroid/fdroid/data/AppProvider.java @@ -416,6 +416,10 @@ public class AppProvider extends FDroidProvider { throw new UnsupportedOperationException("Invalid URI for app content provider: " + uri); } + if (AppProvider.DataColumns.NAME.equals(sortOrder)) { + sortOrder = " lower( " + sortOrder + " ) "; + } + for (String field : projection) { if (field.equals(DataColumns._COUNT)) { projection = new String[] { "COUNT(*) AS " + DataColumns._COUNT }; From 81359f929eaec73371d3dd625383546d87fd41e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 11 Feb 2014 09:06:36 +0100 Subject: [PATCH 077/282] Place top categories in the original order --- src/org/fdroid/fdroid/data/AppProvider.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/org/fdroid/fdroid/data/AppProvider.java b/src/org/fdroid/fdroid/data/AppProvider.java index 31db1b37e..d3db88326 100644 --- a/src/org/fdroid/fdroid/data/AppProvider.java +++ b/src/org/fdroid/fdroid/data/AppProvider.java @@ -76,11 +76,11 @@ public class AppProvider extends FDroidProvider { Collections.sort(categories); // Populate the category list with the real categories, and the - // locally generated meta-categories for "All", "What's New" and - // "Recently Updated"... + // locally generated meta-categories for "What's New", "Recently + // Updated" and "All"... + categories.add(0, getCategoryAll(context)); categories.add(0, getCategoryRecentlyUpdated(context)); categories.add(0, getCategoryWhatsNew(context)); - categories.add(0, getCategoryAll(context)); return categories; } From 9b2e5c2426ff4e7647a15197e41b22c212ae3ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 12 Feb 2014 19:10:47 +0100 Subject: [PATCH 078/282] Don't let AppDetails header textviews overflow --- res/layout/appdetails.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/res/layout/appdetails.xml b/res/layout/appdetails.xml index 455466134..69ff4303f 100644 --- a/res/layout/appdetails.xml +++ b/res/layout/appdetails.xml @@ -32,6 +32,8 @@ android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentRight="true" + android:singleLine="true" + android:ellipsize="end" android:paddingTop="3sp" android:paddingBottom="3sp" android:layout_marginLeft="8sp" @@ -41,6 +43,8 @@ android:id="@+id/categories" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:singleLine="true" + android:ellipsize="end" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:layout_marginLeft="8sp" @@ -61,6 +65,8 @@ android:id="@+id/status" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:singleLine="true" + android:ellipsize="end" android:textSize="13sp" android:layout_alignParentLeft="true" android:layout_alignParentBottom="true" From 9bd236d66c0bb1ff826895742f5f18440bf5a350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 12 Feb 2014 19:30:26 +0100 Subject: [PATCH 079/282] Some much needed fixes to appdetails headers * If the info is taller than the icon, grow larger to fit it in * Center icon vertically * Move padding out of the header * Revert some font sizes to how they were some time ago, a bit smaller --- res/layout/appdetails.xml | 41 +++++++++++++++++++-------------------- res/layout/appinfo.xml | 7 +++++-- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/res/layout/appdetails.xml b/res/layout/appdetails.xml index 69ff4303f..19dcdfbd4 100644 --- a/res/layout/appdetails.xml +++ b/res/layout/appdetails.xml @@ -10,19 +10,18 @@ android:id="@+id/header" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:paddingBottom="4dp" android:orientation="horizontal" > @@ -34,21 +33,8 @@ android:layout_alignParentRight="true" android:singleLine="true" android:ellipsize="end" - android:paddingTop="3sp" - android:paddingBottom="3sp" - android:layout_marginLeft="8sp" - android:textSize="13sp" /> - - + android:layout_marginLeft="6sp" + android:textSize="12sp" /> + + + android:layout_toLeftOf="@id/categories" + android:layout_below="@id/title" /> + + diff --git a/res/layout/appinfo.xml b/res/layout/appinfo.xml index 546d06bed..b567185c5 100644 --- a/res/layout/appinfo.xml +++ b/res/layout/appinfo.xml @@ -2,7 +2,8 @@ @@ -16,7 +17,7 @@ android:id="@+id/appid" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:textSize="13sp" /> + android:textSize="12sp" /> From f35528cd78c8cace026cacd6fce7097c8bba8b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 12 Feb 2014 20:15:43 +0100 Subject: [PATCH 080/282] Fix remaining issues related to vertical length and centering --- res/layout/appdetails.xml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/res/layout/appdetails.xml b/res/layout/appdetails.xml index 19dcdfbd4..e57068f4d 100644 --- a/res/layout/appdetails.xml +++ b/res/layout/appdetails.xml @@ -6,23 +6,27 @@ android:baselineAligned="false" android:orientation="vertical" > - + android:baselineAligned="false" > + android:scaleType="center" /> - + Date: Wed, 12 Feb 2014 20:23:19 +0100 Subject: [PATCH 081/282] Apply similar layout improvement to app list elements --- res/layout/appdetails.xml | 2 +- res/layout/applistitem.xml | 46 ++++++++++++++++++-------------------- res/values/dimen.xml | 4 ++-- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/res/layout/appdetails.xml b/res/layout/appdetails.xml index e57068f4d..9f3fcd070 100644 --- a/res/layout/appdetails.xml +++ b/res/layout/appdetails.xml @@ -42,7 +42,7 @@ + android:layout_width="56dp" + android:layout_height="56dp" + android:layout_centerVertical="true" + android:padding="4dp" + android:scaleType="center" /> - - - + + diff --git a/res/values/dimen.xml b/res/values/dimen.xml index 2ba4e3cd3..14b879442 100644 --- a/res/values/dimen.xml +++ b/res/values/dimen.xml @@ -1,7 +1,7 @@ - 64dp + 56dp - 48dp + 40dp From a3b316c4a8cc400fa21873d224274afde0f75f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 12 Feb 2014 20:46:32 +0100 Subject: [PATCH 082/282] Fix a couple regressions --- res/layout/applistitem.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/res/layout/applistitem.xml b/res/layout/applistitem.xml index 984627404..f01cc5ed6 100644 --- a/res/layout/applistitem.xml +++ b/res/layout/applistitem.xml @@ -13,13 +13,14 @@ android:layout_height="56dp" android:layout_centerVertical="true" android:padding="4dp" - android:scaleType="center" /> + android:scaleType="fitCenter" /> From 2f6a812fdcbd43b96505758c9578bc898a397d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 12 Feb 2014 20:53:26 +0100 Subject: [PATCH 083/282] Fix db rewrite regression, filtering pref used the wrong way --- src/org/fdroid/fdroid/AppFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/fdroid/fdroid/AppFilter.java b/src/org/fdroid/fdroid/AppFilter.java index ff2be9a46..24f723d70 100644 --- a/src/org/fdroid/fdroid/AppFilter.java +++ b/src/org/fdroid/fdroid/AppFilter.java @@ -29,9 +29,9 @@ public class AppFilter { // preferences, and false otherwise. public boolean filter(App app) { - boolean filterRequiringRoot = Preferences.get().filterAppsRequiringRoot(); + boolean dontFilterRequiringRoot = Preferences.get().filterAppsRequiringRoot(); - if (app.requirements == null || !filterRequiringRoot) return false; + if (app.requirements == null || dontFilterRequiringRoot) return false; for (String r : app.requirements) { if (r.equals("root")) From ae10cd0db4527bf1b131b05043a9938c56823a8f Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 29 Jan 2014 21:21:57 -0500 Subject: [PATCH 084/282] save/restore current category in Available view This saves the currently selected category in the Available apps view, and restores that category when the user returns to the Available screen. It drives me totally nuts that it always forgets the category when I nav away from that screen, always returning to What's New. --- .../fragments/AvailableAppsFragment.java | 64 +++++++++++++++---- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java index 7fb0bae68..ee8efedd3 100644 --- a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java @@ -1,11 +1,12 @@ package org.fdroid.fdroid.views.fragments; +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; -import android.os.Build; import android.os.Bundle; -import android.os.Handler; import android.support.v4.app.LoaderManager; import android.util.Log; import android.view.LayoutInflater; @@ -23,7 +24,11 @@ import java.util.List; public class AvailableAppsFragment extends AppListFragment implements LoaderManager.LoaderCallbacks { + public static final String PREFERENCES_FILE = "CategorySpinnerPosition"; + public static final String CATEGORY_KEY = "Selection"; + public static String DEFAULT_CATEGORY; + private Spinner categorySpinner; private String currentCategory = null; private AppListAdapter adapter = null; @@ -80,35 +85,31 @@ public class AvailableAppsFragment extends AppListFragment implements final List categories = AppProvider.Helper.categories(getActivity()); - Spinner spinner = new Spinner(getActivity()); + categorySpinner = new Spinner(getActivity()); // Giving it an ID lets the default save/restore state // functionality do its stuff. - spinner.setId(R.id.categorySpinner); + categorySpinner.setId(R.id.categorySpinner); ArrayAdapter adapter = new ArrayAdapter( getActivity(), android.R.layout.simple_spinner_item, categories); adapter.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item); - spinner.setAdapter(adapter); + categorySpinner.setAdapter(adapter); getActivity().getContentResolver().registerContentObserver( AppProvider.getContentUri(), false, new CategoryObserver(adapter)); - spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + categorySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int pos, long id) { - currentCategory = categories.get(pos); - Log.d("FDroid", "Category '" + currentCategory + "' selected."); - getLoaderManager().restartLoader(0, null, AvailableAppsFragment.this); + setCurrentCategory(categories.get(pos)); } @Override public void onNothingSelected(AdapterView parent) { - currentCategory = null; - Log.d("FDroid", "Select empty category."); - getLoaderManager().restartLoader(0, null, AvailableAppsFragment.this); + setCurrentCategory(null); } }); - return spinner; + return categorySpinner; } @Override @@ -132,6 +133,9 @@ public class AvailableAppsFragment extends AppListFragment implements LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)); + // R.string.category_whatsnew is the default set in AppListManager + DEFAULT_CATEGORY = getActivity().getString(R.string.category_whatsnew); + return view; } @@ -146,4 +150,38 @@ public class AvailableAppsFragment extends AppListFragment implements else return AppProvider.getCategoryUri(currentCategory); } + + private void setCurrentCategory(String category) { + currentCategory = category; + Log.d("FDroid", "Category '" + currentCategory + "' selected."); + getLoaderManager().restartLoader(0, null, AvailableAppsFragment.this); + } + + @Override + public void onResume() { + super.onResume(); + /* restore the saved Category Spinner position */ + Activity activity = getActivity(); + SharedPreferences p = activity.getSharedPreferences(PREFERENCES_FILE, + Context.MODE_PRIVATE); + currentCategory = p.getString(CATEGORY_KEY, DEFAULT_CATEGORY); + for (int i = 0; i < categorySpinner.getCount(); i++) { + if (currentCategory.equals(categorySpinner.getItemAtPosition(i).toString())) { + categorySpinner.setSelection(i); + break; + } + } + setCurrentCategory(currentCategory); + } + + @Override + public void onPause() { + super.onPause(); + /* store the Category Spinner position for when we come back */ + SharedPreferences p = getActivity().getSharedPreferences(PREFERENCES_FILE, + Context.MODE_PRIVATE); + SharedPreferences.Editor e = p.edit(); + e.putString(CATEGORY_KEY, currentCategory); + e.commit(); + } } From fc511fd94fd3b22dd80a44e9cd681fce94502dfe Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 30 Jan 2014 21:25:50 -0500 Subject: [PATCH 085/282] use https for fdroid.org everywhere, avoid redirects --- res/values-ca/strings.xml | 2 +- res/values-el/strings.xml | 2 +- res/values-fr/strings.xml | 2 +- res/values-pt-rBR/strings.xml | 2 +- res/values-sr/strings.xml | 2 +- res/values-ug/strings.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index eed37b7a4..cccc8a333 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -29,7 +29,7 @@ Publicat amb la llicència GNU GPL v3. Un dipòsit és una font d\'aplicacions. Per afegir-ne un, premeu ara el botó MENÚ i entreu la seva URL. -L\'adreça d\'un dipòsit té un aspecte com ara: http://f-droid.org/repo +L\'adreça d\'un dipòsit té un aspecte com ara: https://f-droid.org/repo Instal·lat No està instal·lat S\'ha afegit a %s diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index 6a2b5e8f7..80f806187 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -29,7 +29,7 @@ Ένα αποθετήριο είναι μια πηγή εφαρμογών. Για να προσθέσετε κάποιο, πιέστε το πλήκτρο ΜΕΝΟΥ και εισάγετε το URL. -Μια διεύθυνση αποθετηρίου μοιάζει κάπως έτσι: http://f-droid.org/repo +Μια διεύθυνση αποθετηρίου μοιάζει κάπως έτσι: https://f-droid.org/repo Εγκατεστημένο Δεν είναι εγκατεστημένο Προστέθηκε στις %s diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 51677523d..2796c6b36 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -29,7 +29,7 @@ Publiée sous licence GNU GPL v3. Un dépôt est une source d\'applications. Pour en ajouter un, appuyez maintenant sur le bouton MENU et entrez l\'adresse URL. -L\'URL d\'un dépôt ressemble à ceci : http://f-droid.org/repo +L\'URL d\'un dépôt ressemble à ceci : https://f-droid.org/repo Installée Pas installée Ajouté le %s diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index 6c3b972bc..72164cc15 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -29,7 +29,7 @@ Lançado sob a licença GNU GPLv3. Um repositório é uma fonte de aplicativos. Para adicionar um, pressione o botão MENU e digite a URL. -Um endereço do repositório é algo similar a isto: http://f-droid.org/repo +Um endereço do repositório é algo similar a isto: https://f-droid.org/repo Instalado Não Instalado Adicionado em %s diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index fad6b6304..faf2f69a6 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -29,7 +29,7 @@ Ризнице су места одакле се скидају апликације. Да би сте додали једну, притисните тастер МЕНИ и унесите адресу. -Адреса ризнице би личила на ово: http://f-droid.org/repo +Адреса ризнице би личила на ово: https://f-droid.org/repo Инсталирана Није Инсталирана Додато %s diff --git a/res/values-ug/strings.xml b/res/values-ug/strings.xml index 53c8c774b..0b1921925 100644 --- a/res/values-ug/strings.xml +++ b/res/values-ug/strings.xml @@ -29,7 +29,7 @@ خەزىنە ئەپلەرنىڭ تارقىتىلىش مەنبەسى بولۇپ، مەنبە قوشۇشتا، تىزىملىك توپچىنى بېسىپ، ئاندىن URLنى كىرگۈزۈڭ. -خەزىنە ئادرېسى بۇنىڭغا ئوخشاش بولىدۇ: http://f-droid.org/repo +خەزىنە ئادرېسى بۇنىڭغا ئوخشاش بولىدۇ: https://f-droid.org/repo ئورنىتىلغان ئورنىتىلمىغان %s دا قوشۇلغان From 220b3d1441fb317581e0918e59a6f59730f7fee2 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 31 Jan 2014 20:47:57 -0500 Subject: [PATCH 086/282] ensure repo fingerprints are always stored in all upper case this makes sure that the repo fingerprints are always going to have the same case, no matter how they were added. Repo.fingerprint probably should be converted to a BigInteger so that the comparison can be numeric rather than String. Then when the fingerprint needs to be displayed, it can be formatted appropriately. --- src/org/fdroid/fdroid/ManageRepo.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/ManageRepo.java b/src/org/fdroid/fdroid/ManageRepo.java index 519ef2b80..d46d2b093 100644 --- a/src/org/fdroid/fdroid/ManageRepo.java +++ b/src/org/fdroid/fdroid/ManageRepo.java @@ -423,6 +423,7 @@ class RepoListFragment extends ListFragment final Button addButton = alrt.getButton(DialogInterface.BUTTON_POSITIVE); alrt.setTitle(R.string.repo_exists); overwriteMessage.setVisibility(View.VISIBLE); + newFingerprint = newFingerprint.toUpperCase(Locale.ENGLISH); if (repo.fingerprint == null && newFingerprint != null) { // we're upgrading from unsigned to signed repo overwriteMessage.setText(R.string.repo_exists_add_fingerprint); @@ -469,7 +470,7 @@ class RepoListFragment extends ListFragment private void createNewRepo(String address, String fingerprint) { ContentValues values = new ContentValues(2); values.put(RepoProvider.DataColumns.ADDRESS, address); - values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint); + values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint.toUpperCase(Locale.ENGLISH)); RepoProvider.Helper.insert(getActivity().getContentResolver(), values); finishedAddingRepo(); } From 4489037619d4d75a475ab3698eaf810507593e40 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 31 Jan 2014 21:49:13 -0500 Subject: [PATCH 087/282] NFC beam the repo in RepoDetailsActivity This is the framework for easily swapping repos. The idea is that a user can send the URL with the fingerprint for trusted bootstrapping of the repo on a new user's device. This will be essential for p2p repos provided by Bazaar/Kerplapp. The required NFC APIs were introduced in android-14. So android-14 and below skip the NFC stuff. --- AndroidManifest.xml | 30 +++++++- .../fdroid/views/RepoDetailsActivity.java | 75 ++++++++++++++++++- 2 files changed, 100 insertions(+), 5 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 56149a5c2..d983b6dda 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -20,12 +20,16 @@ + + -
+ + + + + + + + + + + + + + + + + + - + = 14) + setNfc(); + } + + @TargetApi(14) + private void setNfc() { + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); + if (nfcAdapter == null) { + return; + } + nfcAdapter.setNdefPushMessage(new NdefMessage(new NdefRecord[] { + NdefRecord.createUri(getSharingUri()), + }), this); + findViewById(android.R.id.content).post(new Runnable() { + @Override + public void run() { + Log.i(TAG, "Runnable.run()"); + onNewIntent(getIntent()); + } + }); + } + + @Override + public void onResume() { + Log.i(TAG, "onResume"); + super.onResume(); + if (Build.VERSION.SDK_INT >= 9) + processIntent(getIntent()); + } + + @Override + public void onNewIntent(Intent i) { + Log.i(TAG, "onNewIntent"); + Log.i(TAG, "action: " + i.getAction()); + Log.i(TAG, "data: " + i.getData()); + // onResume gets called after this to handle the intent + setIntent(i); + } + + @TargetApi(9) + void processIntent(Intent i) { + if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(i.getAction())) { + Log.i(TAG, "ACTION_NDEF_DISCOVERED"); + Parcelable[] rawMsgs = + i.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); + NdefMessage msg = (NdefMessage) rawMsgs[0]; + String url = new String(msg.getRecords()[0].getPayload()); + Log.i(TAG, "Got this URL: " + url); + Toast.makeText(this, "Got this URL: " + url, Toast.LENGTH_LONG).show(); + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + String packageName = getPackageName(); + intent.setClassName(packageName, packageName + ".ManageRepo"); + startActivity(intent); + finish(); + } } protected Uri getSharingUri() { From ea7f82ed1a031af1fdab4b904e533b020628f6c1 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 30 Jan 2014 16:16:56 -0500 Subject: [PATCH 088/282] add menu item to enable NFC to RepoDetails view It is now possible to beam a repo config via NFC but just selecting the repo in FDroid, then touching two NFC devices together, and clicking on the FDroid one. There is no indication that NFC is off, so this commit adds a menu item that makes it easy to enable the required NFC settings for sending a repo to another device via NFC. --- res/values/strings.xml | 3 ++ .../fdroid/fdroid/NfcNotEnabledActivity.java | 43 +++++++++++++++ .../views/fragments/RepoDetailsFragment.java | 52 ++++++++++++++++--- 3 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 src/org/fdroid/fdroid/NfcNotEnabledActivity.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 3ded618fa..0895ad430 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9,6 +9,7 @@ Version Edit Delete + Enable NFC Send… App cache Keep downloaded apk files on SD card Do not keep any apk files @@ -73,6 +74,8 @@ Please Wait Updating application list… Getting application from + NFC is not enabled! + Go to NFC Settings… Repository address Fingerprint (optional) diff --git a/src/org/fdroid/fdroid/NfcNotEnabledActivity.java b/src/org/fdroid/fdroid/NfcNotEnabledActivity.java new file mode 100644 index 000000000..711a17f97 --- /dev/null +++ b/src/org/fdroid/fdroid/NfcNotEnabledActivity.java @@ -0,0 +1,43 @@ + +package org.fdroid.fdroid; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Intent; +import android.nfc.NfcAdapter; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; + +@TargetApi(14) +// aka Android 4.0 aka Ice Cream Sandwich +public class NfcNotEnabledActivity extends Activity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Intent intent = new Intent(); + if (Build.VERSION.SDK_INT >= 16) { + /* + * ACTION_NFC_SETTINGS was added in 4.1 aka Jelly Bean MR1 as a + * separate thing from ACTION_NFCSHARING_SETTINGS. It is now + * possible to have NFC enabled, but not "Android Beam", which is + * needed for NDEF. Therefore, we detect the current state of NFC, + * and steer the user accordingly. + */ + if (NfcAdapter.getDefaultAdapter(this).isEnabled()) + intent.setAction(Settings.ACTION_NFCSHARING_SETTINGS); + else + intent.setAction(Settings.ACTION_NFC_SETTINGS); + } else if (Build.VERSION.SDK_INT >= 14) { + // this API was added in 4.0 aka Ice Cream Sandwich + intent.setAction(Settings.ACTION_NFCSHARING_SETTINGS); + } else { + // no NFC support, so nothing to do here + finish(); + return; + } + startActivity(intent); + finish(); + } +} diff --git a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java index d86807c0d..4e762d238 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java @@ -1,9 +1,13 @@ package org.fdroid.fdroid.views.fragments; +import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.content.ContentValues; import android.content.DialogInterface; +import android.content.Intent; +import android.nfc.NfcAdapter; +import android.os.Build; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; @@ -50,6 +54,9 @@ public class RepoDetailsFragment extends Fragment { private static final int DELETE = 0; private static final int UPDATE = 1; + private static final int ENABLE_NFC = 2; + + private MenuItem enableNfc = null; // TODO: Currently initialised in onCreateView. Not sure if that is the // best way to go about this... @@ -244,18 +251,51 @@ public class RepoDetailsFragment extends Fragment { MenuItemCompat.setShowAsAction(delete, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT); + } + @Override + public void onPrepareOptionsMenu(Menu menu) { + if (Build.VERSION.SDK_INT >= 14) + prepareNfcMenuItems(menu); + } + + @TargetApi(16) + private void prepareNfcMenuItems(Menu menu) { + boolean needsEnableNfcMenuItem = false; + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity()); + if (Build.VERSION.SDK_INT < 16) + needsEnableNfcMenuItem = !nfcAdapter.isEnabled(); + else + needsEnableNfcMenuItem = !nfcAdapter.isNdefPushEnabled(); + if (needsEnableNfcMenuItem) { + if (enableNfc != null) + return; // already created + enableNfc = menu.add(Menu.NONE, ENABLE_NFC, 0, R.string.enable_nfc_send); + enableNfc.setIcon(android.R.drawable.ic_menu_preferences); + MenuItemCompat.setShowAsAction(enableNfc, + MenuItemCompat.SHOW_AS_ACTION_IF_ROOM | + MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT); + } else if (enableNfc != null) { + // remove the existing MenuItem since NFC is now enabled + menu.removeItem(enableNfc.getItemId()); + enableNfc = null; + } } @Override public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == DELETE) { - promptForDelete(); - return true; - } else if (item.getItemId() == UPDATE) { - performUpdate(); - return true; + switch (item.getItemId()) { + case DELETE: + promptForDelete(); + return true; + case UPDATE: + performUpdate(); + return true; + case ENABLE_NFC: + Intent intent = new Intent(getActivity(), NfcNotEnabledActivity.class); + startActivity(intent); + return true; } return false; From ba8de64686ad813be5252f1db460049001d9aab8 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 31 Jan 2014 21:08:03 -0500 Subject: [PATCH 089/282] split out RepoListFragment from ManageRepo, Fragments need to be public Otherwise we get errors like this upon rotation: "android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment org.fdroid.fdroid.RepoListFragment: make sure class name exists, is public, and has an empty constructor that is public" --- src/org/fdroid/fdroid/ManageRepo.java | 461 +---------------- .../views/fragments/RepoListFragment.java | 474 ++++++++++++++++++ 2 files changed, 477 insertions(+), 458 deletions(-) create mode 100644 src/org/fdroid/fdroid/views/fragments/RepoListFragment.java diff --git a/src/org/fdroid/fdroid/ManageRepo.java b/src/org/fdroid/fdroid/ManageRepo.java index d46d2b093..da66d4f7f 100644 --- a/src/org/fdroid/fdroid/ManageRepo.java +++ b/src/org/fdroid/fdroid/ManageRepo.java @@ -20,42 +20,15 @@ package org.fdroid.fdroid; import android.app.Activity; -import android.app.AlertDialog; -import android.content.*; -import android.preference.PreferenceManager; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.ListFragment; -import android.support.v4.content.CursorLoader; -import android.database.Cursor; -import android.net.Uri; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; +import android.content.Intent; import android.os.Bundle; -import android.support.v4.app.LoaderManager; +import android.support.v4.app.FragmentActivity; import android.support.v4.app.NavUtils; -import android.support.v4.content.Loader; -import android.support.v4.view.MenuItemCompat; -import android.text.TextUtils; -import android.text.format.DateFormat; import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; -import android.view.View; -import android.widget.*; import org.fdroid.fdroid.compat.ActionBarCompat; -import org.fdroid.fdroid.compat.ClipboardCompat; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; -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.Date; -import java.util.Locale; +import org.fdroid.fdroid.views.fragments.RepoListFragment; public class ManageRepo extends FragmentActivity { @@ -105,432 +78,4 @@ public class ManageRepo extends FragmentActivity { } return super.onOptionsItemSelected(item); } - -} - -class RepoListFragment extends ListFragment - implements LoaderManager.LoaderCallbacks,RepoAdapter.EnabledListener { - - private static final String DEFAULT_NEW_REPO_TEXT = "https://"; - private final int ADD_REPO = 1; - private final int UPDATE_REPOS = 2; - - private WifiManager wifiManager; - - public boolean hasChanged() { - return changed; - } - - @Override - public Loader onCreateLoader(int i, Bundle bundle) { - Uri uri = RepoProvider.getContentUri(); - Log.i("FDroid", "Creating repo loader '" + uri + "'."); - String[] projection = new String[] { - RepoProvider.DataColumns._ID, - RepoProvider.DataColumns.NAME, - RepoProvider.DataColumns.PUBLIC_KEY, - RepoProvider.DataColumns.FINGERPRINT, - RepoProvider.DataColumns.IN_USE - }; - return new CursorLoader(getActivity(), uri, projection, null, null, null); - } - - @Override - public void onLoadFinished(Loader cursorLoader, Cursor cursor) { - Log.i("FDroid", "Repo cursor loaded."); - repoAdapter.swapCursor(cursor); - } - - @Override - public void onLoaderReset(Loader cursorLoader) { - Log.i("FDroid", "Repo cursor reset."); - repoAdapter.swapCursor(null); - } - - - /** - * 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. - */ - @Override - public void onSetEnabled(Repo repo, boolean isEnabled) { - if (repo.inuse != isEnabled ) { - ContentValues values = new ContentValues(1); - values.put(RepoProvider.DataColumns.IN_USE, isEnabled ? 1 : 0); - RepoProvider.Helper.update( - getActivity().getContentResolver(), repo, values); - - if (isEnabled) { - changed = true; - } else { - FDroidApp app = (FDroidApp)getActivity().getApplication(); - RepoProvider.Helper.purgeApps(getActivity(), repo, app); - String notification = getString(R.string.repo_disabled_notification, repo.name); - Toast.makeText(getActivity(), notification, Toast.LENGTH_LONG).show(); - } - } - } - - private enum PositiveAction { - ADD_NEW, ENABLE, IGNORE - } - private PositiveAction positiveAction; - - private boolean changed = false; - - private RepoAdapter repoAdapter; - - /** - * 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; - - private View createHeaderView() { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); - TextView textLastUpdate = new TextView(getActivity()); - long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0); - String lastUpdateCheck = ""; - if (lastUpdate == 0) { - lastUpdateCheck = getString(R.string.never); - } else { - Date d = new Date(lastUpdate); - lastUpdateCheck = DateFormat.getDateFormat(getActivity()).format(d) + - " " + DateFormat.getTimeFormat(getActivity()).format(d); - } - textLastUpdate.setText(getString(R.string.last_update_check, lastUpdateCheck)); - - int sidePadding = (int)getResources().getDimension(R.dimen.padding_side); - int topPadding = (int)getResources().getDimension(R.dimen.padding_top); - - textLastUpdate.setPadding(sidePadding, topPadding, sidePadding, topPadding); - return textLastUpdate; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - // Can't do this in the onCreate view, because "onCreateView" which - // returns the list view is "called between onCreate and - // onActivityCreated" according to the docs. - getListView().addHeaderView(createHeaderView()); - - // This could go in onCreate (and used to) but it needs to be called - // after addHeaderView, which can only be called after onCreate... - repoAdapter = new RepoAdapter(getActivity(), null); - repoAdapter.setEnabledListener(this); - setListAdapter(repoAdapter); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - - super.onCreate(savedInstanceState); - - setHasOptionsMenu(true); - - /* let's see if someone is trying to send us a new repo */ - Intent intent = getActivity().getIntent(); - /* an URL from a click, NFC, QRCode scan, etc */ - Uri uri = intent.getData(); - if (uri != null) { - // scheme and host should only ever be pure ASCII aka Locale.ENGLISH - String scheme = intent.getScheme(); - String host = uri.getHost(); - if (scheme == null || host == null) { - String msg = String.format(getString(R.string.malformed_repo_uri), uri); - Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG).show(); - return; - } - if (scheme.equals("FDROIDREPO") || scheme.equals("FDROIDREPOS")) { - /* - * QRCodes are more efficient in all upper case, so QR URIs are - * encoded in all upper case, then forced to lower case. - * Checking if the special F-Droid scheme being all is upper - * case means it should be downcased. - */ - uri = Uri.parse(uri.toString().toLowerCase(Locale.ENGLISH)); - } - // make scheme and host lowercase so they're readable in dialogs - scheme = scheme.toLowerCase(Locale.ENGLISH); - host = host.toLowerCase(Locale.ENGLISH); - String fingerprint = uri.getQueryParameter("fingerprint"); - if (scheme.equals("fdroidrepos") || scheme.equals("fdroidrepo") - || scheme.equals("https") || scheme.equals("http")) { - - isImportingRepo = true; - - /* sanitize and format for function and readability */ - String uriString = uri.toString() - .replaceAll("\\?.*$", "") // remove the whole query - .replaceAll("/*$", "") // remove all trailing slashes - .replace(uri.getHost(), host) // downcase host name - .replace(intent.getScheme(), scheme) // downcase scheme - .replace("fdroidrepo", "http"); // proper repo address - showAddRepo(uriString, fingerprint); - - // if this is a local repo, check we're on the same wifi - String uriBssid = uri.getQueryParameter("bssid"); - if (!TextUtils.isEmpty(uriBssid)) { - if (uri.getPort() != 8888 - && !host.matches("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+")) { - Log.i("ManageRepo", "URI is not local repo: " + uri); - return; - } - Activity a = getActivity(); - if (wifiManager == null) - wifiManager = (WifiManager) a.getSystemService(Context.WIFI_SERVICE); - WifiInfo wifiInfo = wifiManager.getConnectionInfo(); - String bssid = wifiInfo.getBSSID().toLowerCase(Locale.ENGLISH); - uriBssid = Uri.decode(uriBssid).toLowerCase(Locale.ENGLISH); - if (!bssid.equals(uriBssid)) { - String msg = String.format(getString(R.string.not_on_same_wifi), - uri.getQueryParameter("ssid")); - Toast.makeText(a, msg, Toast.LENGTH_LONG).show(); - } - // TODO we should help the user to the right thing here, - // instead of just showing a message! - } - } - } - } - - @Override - public void onResume() { - super.onResume(); - - //Starts a new or restarts an existing Loader in this manager - getLoaderManager().restartLoader(0, null, this); - } - - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - - super.onListItemClick(l, v, position, id); - - Repo repo = new Repo((Cursor)getListView().getItemAtPosition(position)); - editRepo(repo); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - - 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); - } - - public static final int SHOW_REPO_DETAILS = 1; - - public void editRepo(Repo repo) { - Intent intent = new Intent(getActivity(), RepoDetailsActivity.class); - intent.putExtra(RepoDetailsFragment.ARG_REPO_ID, repo.getId()); - startActivityForResult(intent, SHOW_REPO_DETAILS); - } - - private void updateRepos() { - 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) { - // No need to prompt to update any more, we just did it! - changed = false; - } - } - }); - } - - private void showAddRepo() { - showAddRepo(getNewRepoUri(), null); - } - - private void showAddRepo(String newAddress, String newFingerprint) { - View view = getLayoutInflater(null).inflate(R.layout.addrepo, null); - final AlertDialog alrt = new AlertDialog.Builder(getActivity()).setView(view).create(); - final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri); - final EditText fingerprintEditText = (EditText) view.findViewById(R.id.edit_fingerprint); - - final Repo repo = ( newAddress != null && isImportingRepo ) - ? RepoProvider.Helper.findByAddress(getActivity().getContentResolver(), newAddress) - : null; - - alrt.setIcon(android.R.drawable.ic_menu_add); - alrt.setTitle(getString(R.string.repo_add_title)); - alrt.setButton(DialogInterface.BUTTON_POSITIVE, - getString(R.string.repo_add_add), - 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) - createNewRepo(uriEditText.getText().toString(), fp); - else if (positiveAction == PositiveAction.ENABLE) - createNewRepo(repo); - } - }); - - alrt.setButton(DialogInterface.BUTTON_NEGATIVE, - getString(R.string.cancel), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - getActivity().setResult(Activity.RESULT_CANCELED); - getActivity().finish(); - return; - } - }); - alrt.show(); - - final TextView overwriteMessage = (TextView) view.findViewById(R.id.overwrite_message); - overwriteMessage.setVisibility(View.GONE); - if (repo == null) { - // no existing repo, add based on what we have - positiveAction = PositiveAction.ADD_NEW; - } else { - // found the address in the DB of existing repos - final Button addButton = alrt.getButton(DialogInterface.BUTTON_POSITIVE); - alrt.setTitle(R.string.repo_exists); - overwriteMessage.setVisibility(View.VISIBLE); - newFingerprint = newFingerprint.toUpperCase(Locale.ENGLISH); - if (repo.fingerprint == null && newFingerprint != null) { - // we're upgrading from unsigned to signed repo - overwriteMessage.setText(R.string.repo_exists_add_fingerprint); - addButton.setText(R.string.add_key); - positiveAction = PositiveAction.ADD_NEW; - } else if (newFingerprint == null || newFingerprint.equals(repo.fingerprint)) { - // this entry already exists and is not enabled, offer to enable it - if (repo.inuse) { - alrt.dismiss(); - Toast.makeText(getActivity(), R.string.repo_exists_and_enabled, Toast.LENGTH_LONG).show(); - return; - } else { - overwriteMessage.setText(R.string.repo_exists_enable); - addButton.setText(R.string.enable); - positiveAction = PositiveAction.ENABLE; - } - } else { - // same address with different fingerprint, this could be - // malicious, so force the user to manually delete the repo - // before adding this one - overwriteMessage.setTextColor(getResources().getColor(R.color.red)); - overwriteMessage.setText(R.string.repo_delete_to_overwrite); - addButton.setText(R.string.overwrite); - addButton.setEnabled(false); - positiveAction = PositiveAction.IGNORE; - } - } - - 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); - } - } - - /** - * Adds a new repo to the database. - */ - private void createNewRepo(String address, String fingerprint) { - ContentValues values = new ContentValues(2); - values.put(RepoProvider.DataColumns.ADDRESS, address); - values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint.toUpperCase(Locale.ENGLISH)); - RepoProvider.Helper.insert(getActivity().getContentResolver(), values); - finishedAddingRepo(); - } - - /** - * Seeing as this repo already exists, we will force it to be enabled again. - */ - private void createNewRepo(Repo repo) { - ContentValues values = new ContentValues(1); - values.put(RepoProvider.DataColumns.IN_USE, 1); - RepoProvider.Helper.update(getActivity().getContentResolver(), repo, values); - repo.inuse = true; - 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; - if (isImportingRepo) { - getActivity().setResult(Activity.RESULT_OK); - getActivity().finish(); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - - if (item.getItemId() == ADD_REPO) { - showAddRepo(); - return true; - } else if (item.getItemId() == UPDATE_REPOS) { - updateRepos(); - return true; - } - - return super.onOptionsItemSelected(item); - } - - /** - * 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(getActivity()); - 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; - } } diff --git a/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java new file mode 100644 index 000000000..f1f0555c9 --- /dev/null +++ b/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java @@ -0,0 +1,474 @@ + +package org.fdroid.fdroid.views.fragments; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v4.app.ListFragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.view.MenuItemCompat; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.ProgressListener; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.UpdateService; +import org.fdroid.fdroid.compat.ClipboardCompat; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; +import org.fdroid.fdroid.views.RepoAdapter; +import org.fdroid.fdroid.views.RepoDetailsActivity; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Date; +import java.util.Locale; + +public class RepoListFragment extends ListFragment + implements LoaderManager.LoaderCallbacks, RepoAdapter.EnabledListener { + + private static final String DEFAULT_NEW_REPO_TEXT = "https://"; + private final int ADD_REPO = 1; + private final int UPDATE_REPOS = 2; + + private WifiManager wifiManager; + + public boolean hasChanged() { + return changed; + } + + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + Uri uri = RepoProvider.getContentUri(); + Log.i("FDroid", "Creating repo loader '" + uri + "'."); + String[] projection = new String[] { + RepoProvider.DataColumns._ID, + RepoProvider.DataColumns.NAME, + RepoProvider.DataColumns.PUBLIC_KEY, + RepoProvider.DataColumns.FINGERPRINT, + RepoProvider.DataColumns.IN_USE + }; + return new CursorLoader(getActivity(), uri, projection, null, null, null); + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + Log.i("FDroid", "Repo cursor loaded."); + repoAdapter.swapCursor(cursor); + } + + @Override + public void onLoaderReset(Loader cursorLoader) { + Log.i("FDroid", "Repo cursor reset."); + repoAdapter.swapCursor(null); + } + + /** + * 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. + */ + @Override + public void onSetEnabled(Repo repo, boolean isEnabled) { + if (repo.inuse != isEnabled) { + ContentValues values = new ContentValues(1); + values.put(RepoProvider.DataColumns.IN_USE, isEnabled ? 1 : 0); + RepoProvider.Helper.update( + getActivity().getContentResolver(), repo, values); + + if (isEnabled) { + changed = true; + } else { + FDroidApp app = (FDroidApp) getActivity().getApplication(); + RepoProvider.Helper.purgeApps(getActivity(), repo, app); + String notification = getString(R.string.repo_disabled_notification, repo.name); + Toast.makeText(getActivity(), notification, Toast.LENGTH_LONG).show(); + } + } + } + + private enum PositiveAction { + ADD_NEW, ENABLE, IGNORE + } + + private PositiveAction positiveAction; + + private boolean changed = false; + + private RepoAdapter repoAdapter; + + /** + * 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; + + private View createHeaderView() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + TextView textLastUpdate = new TextView(getActivity()); + long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0); + String lastUpdateCheck = ""; + if (lastUpdate == 0) { + lastUpdateCheck = getString(R.string.never); + } else { + Date d = new Date(lastUpdate); + lastUpdateCheck = DateFormat.getDateFormat(getActivity()).format(d) + + " " + DateFormat.getTimeFormat(getActivity()).format(d); + } + textLastUpdate.setText(getString(R.string.last_update_check, lastUpdateCheck)); + + int sidePadding = (int) getResources().getDimension(R.dimen.padding_side); + int topPadding = (int) getResources().getDimension(R.dimen.padding_top); + + textLastUpdate.setPadding(sidePadding, topPadding, sidePadding, topPadding); + return textLastUpdate; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + // Can't do this in the onCreate view, because "onCreateView" which + // returns the list view is "called between onCreate and + // onActivityCreated" according to the docs. + getListView().addHeaderView(createHeaderView()); + + // This could go in onCreate (and used to) but it needs to be called + // after addHeaderView, which can only be called after onCreate... + repoAdapter = new RepoAdapter(getActivity(), null); + repoAdapter.setEnabledListener(this); + setListAdapter(repoAdapter); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + setHasOptionsMenu(true); + + /* let's see if someone is trying to send us a new repo */ + Intent intent = getActivity().getIntent(); + /* an URL from a click, NFC, QRCode scan, etc */ + Uri uri = intent.getData(); + if (uri != null) { + // scheme and host should only ever be pure ASCII aka Locale.ENGLISH + String scheme = intent.getScheme(); + String host = uri.getHost(); + if (scheme == null || host == null) { + String msg = String.format(getString(R.string.malformed_repo_uri), uri); + Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG).show(); + return; + } + if (scheme.equals("FDROIDREPO") || scheme.equals("FDROIDREPOS")) { + /* + * QRCodes are more efficient in all upper case, so QR URIs are + * encoded in all upper case, then forced to lower case. + * Checking if the special F-Droid scheme being all is upper + * case means it should be downcased. + */ + uri = Uri.parse(uri.toString().toLowerCase(Locale.ENGLISH)); + } + // make scheme and host lowercase so they're readable in dialogs + scheme = scheme.toLowerCase(Locale.ENGLISH); + host = host.toLowerCase(Locale.ENGLISH); + String fingerprint = uri.getQueryParameter("fingerprint"); + if (scheme.equals("fdroidrepos") || scheme.equals("fdroidrepo") + || scheme.equals("https") || scheme.equals("http")) { + + isImportingRepo = true; + + /* sanitize and format for function and readability */ + String uriString = uri.toString() + .replaceAll("\\?.*$", "") // remove the whole query + .replaceAll("/*$", "") // remove all trailing slashes + .replace(uri.getHost(), host) // downcase host name + .replace(intent.getScheme(), scheme) // downcase scheme + .replace("fdroidrepo", "http"); // proper repo address + showAddRepo(uriString, fingerprint); + + // if this is a local repo, check we're on the same wifi + String uriBssid = uri.getQueryParameter("bssid"); + if (!TextUtils.isEmpty(uriBssid)) { + if (uri.getPort() != 8888 + && !host.matches("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+")) { + Log.i("ManageRepo", "URI is not local repo: " + uri); + return; + } + Activity a = getActivity(); + if (wifiManager == null) + wifiManager = (WifiManager) a.getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + String bssid = wifiInfo.getBSSID().toLowerCase(Locale.ENGLISH); + uriBssid = Uri.decode(uriBssid).toLowerCase(Locale.ENGLISH); + if (!bssid.equals(uriBssid)) { + String msg = String.format(getString(R.string.not_on_same_wifi), + uri.getQueryParameter("ssid")); + Toast.makeText(a, msg, Toast.LENGTH_LONG).show(); + } + // TODO we should help the user to the right thing here, + // instead of just showing a message! + } + } + } + } + + @Override + public void onResume() { + super.onResume(); + + // Starts a new or restarts an existing Loader in this manager + getLoaderManager().restartLoader(0, null, this); + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + + super.onListItemClick(l, v, position, id); + + Repo repo = new Repo((Cursor) getListView().getItemAtPosition(position)); + editRepo(repo); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + 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); + } + + public static final int SHOW_REPO_DETAILS = 1; + + public void editRepo(Repo repo) { + Intent intent = new Intent(getActivity(), RepoDetailsActivity.class); + intent.putExtra(RepoDetailsFragment.ARG_REPO_ID, repo.getId()); + startActivityForResult(intent, SHOW_REPO_DETAILS); + } + + private void updateRepos() { + 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) { + // No need to prompt to update any more, we just did it! + changed = false; + } + } + }); + } + + private void showAddRepo() { + showAddRepo(getNewRepoUri(), null); + } + + private void showAddRepo(String newAddress, String newFingerprint) { + View view = getLayoutInflater(null).inflate(R.layout.addrepo, null); + final AlertDialog alrt = new AlertDialog.Builder(getActivity()).setView(view).create(); + final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri); + final EditText fingerprintEditText = (EditText) view.findViewById(R.id.edit_fingerprint); + + final Repo repo = (newAddress != null && isImportingRepo) + ? RepoProvider.Helper.findByAddress(getActivity().getContentResolver(), newAddress) + : null; + + alrt.setIcon(android.R.drawable.ic_menu_add); + alrt.setTitle(getString(R.string.repo_add_title)); + alrt.setButton(DialogInterface.BUTTON_POSITIVE, + getString(R.string.repo_add_add), + 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) + createNewRepo(uriEditText.getText().toString(), fp); + else if (positiveAction == PositiveAction.ENABLE) + createNewRepo(repo); + } + }); + + alrt.setButton(DialogInterface.BUTTON_NEGATIVE, + getString(R.string.cancel), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + getActivity().setResult(Activity.RESULT_CANCELED); + getActivity().finish(); + return; + } + }); + alrt.show(); + + final TextView overwriteMessage = (TextView) view.findViewById(R.id.overwrite_message); + overwriteMessage.setVisibility(View.GONE); + if (repo == null) { + // no existing repo, add based on what we have + positiveAction = PositiveAction.ADD_NEW; + } else { + // found the address in the DB of existing repos + final Button addButton = alrt.getButton(DialogInterface.BUTTON_POSITIVE); + alrt.setTitle(R.string.repo_exists); + overwriteMessage.setVisibility(View.VISIBLE); + newFingerprint = newFingerprint.toUpperCase(Locale.ENGLISH); + if (repo.fingerprint == null && newFingerprint != null) { + // we're upgrading from unsigned to signed repo + overwriteMessage.setText(R.string.repo_exists_add_fingerprint); + addButton.setText(R.string.add_key); + positiveAction = PositiveAction.ADD_NEW; + } else if (newFingerprint == null || newFingerprint.equals(repo.fingerprint)) { + // this entry already exists and is not enabled, offer to enable + // it + if (repo.inuse) { + alrt.dismiss(); + Toast.makeText(getActivity(), R.string.repo_exists_and_enabled, + Toast.LENGTH_LONG).show(); + return; + } else { + overwriteMessage.setText(R.string.repo_exists_enable); + addButton.setText(R.string.enable); + positiveAction = PositiveAction.ENABLE; + } + } else { + // same address with different fingerprint, this could be + // malicious, so force the user to manually delete the repo + // before adding this one + overwriteMessage.setTextColor(getResources().getColor(R.color.red)); + overwriteMessage.setText(R.string.repo_delete_to_overwrite); + addButton.setText(R.string.overwrite); + addButton.setEnabled(false); + positiveAction = PositiveAction.IGNORE; + } + } + + 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); + } + } + + /** + * Adds a new repo to the database. + */ + private void createNewRepo(String address, String fingerprint) { + ContentValues values = new ContentValues(2); + values.put(RepoProvider.DataColumns.ADDRESS, address); + values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint.toUpperCase(Locale.ENGLISH)); + RepoProvider.Helper.insert(getActivity().getContentResolver(), values); + finishedAddingRepo(); + } + + /** + * Seeing as this repo already exists, we will force it to be enabled again. + */ + private void createNewRepo(Repo repo) { + ContentValues values = new ContentValues(1); + values.put(RepoProvider.DataColumns.IN_USE, 1); + RepoProvider.Helper.update(getActivity().getContentResolver(), repo, values); + repo.inuse = true; + 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; + if (isImportingRepo) { + getActivity().setResult(Activity.RESULT_OK); + getActivity().finish(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + + if (item.getItemId() == ADD_REPO) { + showAddRepo(); + return true; + } else if (item.getItemId() == UPDATE_REPOS) { + updateRepos(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + /** + * 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(getActivity()); + 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; + } +} From ceed2c31d7c59bf86a31a207324ee8e11d16fc9a Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 31 Jan 2014 21:16:46 -0500 Subject: [PATCH 090/282] prevent crash when using back button after screen rotate To reproduce the crash: 0. click a fdroidrepo:// URI to bring up the "app repo" dialog 1. rotate the device 2. click back to make the keyboard go away 3. click back to make the dialog go away 4. click back on Manage Repos screen 5. boom! --- src/org/fdroid/fdroid/ManageRepo.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/ManageRepo.java b/src/org/fdroid/fdroid/ManageRepo.java index da66d4f7f..757def9db 100644 --- a/src/org/fdroid/fdroid/ManageRepo.java +++ b/src/org/fdroid/fdroid/ManageRepo.java @@ -59,9 +59,10 @@ public class ManageRepo extends FragmentActivity { ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); } + @Override public void finish() { Intent ret = new Intent(); - if (listFragment.hasChanged()) { + if (listFragment != null && listFragment.hasChanged()) { Log.i("FDroid", "Repo details have changed, prompting for update."); ret.putExtra(REQUEST_UPDATE, true); } From 9c9c0a48190f968ee7e847694e90c9cf1523c341 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 12 Feb 2014 20:53:54 -0500 Subject: [PATCH 091/282] setup main FDroid screen to NFC Beam the FDroid.apk This pre-configures a file:// URI that points to the installed location of the FDroid.apk. When users put two devices together, and touch the screen on the device with FDroid on it, it will "beam" over the APK, and prompt the user to install it. --- src/org/fdroid/fdroid/FDroid.java | 40 ++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index e54b8ec25..eaf9db0ac 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -19,28 +19,31 @@ package org.fdroid.fdroid; +import android.annotation.TargetApi; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.NotificationManager; import android.content.*; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; import android.database.ContentObserver; import android.net.Uri; +import android.nfc.NfcAdapter; import android.os.Build; import android.os.Bundle; -import android.os.Handler; +import android.support.v4.app.FragmentActivity; +import android.support.v4.view.MenuItemCompat; +import android.support.v4.view.ViewPager; import android.util.Log; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.*; - -import android.support.v4.app.FragmentActivity; -import android.support.v4.view.MenuItemCompat; -import android.support.v4.view.ViewPager; +import android.widget.TextView; import org.fdroid.fdroid.compat.TabManager; import org.fdroid.fdroid.data.AppProvider; @@ -100,6 +103,14 @@ public class FDroid extends FragmentActivity { getContentResolver().registerContentObserver(uri, true, new AppObserver()); } + @Override + protected void onResume() { + super.onResume(); + // RepoDetailsActivity sets a different beam, so reset here + if (Build.VERSION.SDK_INT >= 16) + setupAndroidBeam(); + } + @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -333,4 +344,21 @@ public class FDroid extends FragmentActivity { } + @TargetApi(16) + private void setupAndroidBeam() { + PackageManager pm = getPackageManager(); + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); + ApplicationInfo appInfo; + try { + appInfo = pm.getApplicationInfo("org.fdroid.fdroid", + PackageManager.GET_META_DATA); + // TODO can we send the repo here also, as a file? + Uri uris[] = { + Uri.parse("file://" + appInfo.publicSourceDir), + }; + nfcAdapter.setBeamPushUris(uris, this); + } catch (NameNotFoundException e1) { + e1.printStackTrace(); + } + } } From 113ae202b7d3a03115fe798bc6268c7aebeeab50 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 13 Feb 2014 21:51:29 -0500 Subject: [PATCH 092/282] include Eclipse project for the embedded Android Test Project This should make it easier for some people get started with the tests. --- test/.project | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/.project diff --git a/test/.project b/test/.project new file mode 100644 index 000000000..0c2b67ff9 --- /dev/null +++ b/test/.project @@ -0,0 +1,33 @@ + + + fdroid-test + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + From 9871ad0f014eeb2d99d74ff1e3e24965ba7cc1b0 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 13 Feb 2014 21:53:52 -0500 Subject: [PATCH 093/282] ant-prepare.sh: also set up test suite, should not affect normal ant builds This adds the command to update the embedded Android Test Project, so that it can be run using `cd test/; ant clean emma debug install test` It also changes -p to --path just to make things a little easier to read. --- ant-prepare.sh | 12 ++++++++---- test/ant.properties | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ant-prepare.sh b/ant-prepare.sh index 7817f4ed2..5463a67d5 100755 --- a/ant-prepare.sh +++ b/ant-prepare.sh @@ -1,6 +1,10 @@ #!/bin/bash -ex -android update lib-project -p extern/Universal-Image-Loader/library -android update lib-project -p extern/AndroidPinning -android update lib-project -p extern/MemorizingTrustManager -android update project -p . --name F-Droid +android update lib-project --path extern/Universal-Image-Loader/library +android update lib-project --path extern/AndroidPinning +android update lib-project --path extern/MemorizingTrustManager +android update project --path . --name F-Droid + +# technically optional, needed for the tests +cd test +android update test-project --path ./ --main ../ diff --git a/test/ant.properties b/test/ant.properties index 7d28fd099..836edf047 100644 --- a/test/ant.properties +++ b/test/ant.properties @@ -15,4 +15,4 @@ # 'key.alias' for the name of the key to use. # The password will be asked during the build when you use the 'release' target. -tested.project.dir=/home/pete/code/fdroid/client +tested.project.dir=../ From cdad2c66ed4cfc8c08b593981c484282b8d46b52 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 13 Feb 2014 23:28:31 -0500 Subject: [PATCH 094/282] add instructions for running the embedded Android Test Project --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 2f8893f6e..9c5d1fbbb 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,29 @@ Translate Extension](http://www.mediawiki.org/wiki/Extension:Translate). See [our translation page](https://f-droid.org/wiki/page/Special:Translate) if you would like to contribute. + +Running the test suite +---------------------- + +FDroid client includes a embedded Android Test Project for running tests. It +is in the `test/` subfolder. To run the tests from the command line, do: + +``` +git submodule update --init +./ant-prepare.sh # This runs 'android update' on the libs and the main project +ant clean emma debug install test +``` + +You can also run the tests in Eclipse. Here's how: + +1. Choose *File* -> *Import* -> *Android* -> *Existing Android Code Into Workspace* for the `fdroidclient/` directory. +2. Choose *File* -> *Import* -> *Android* -> *Existing Android Code Into Workspace* for the `fdroidclient/test/` directory +3. If **fdroid-test** has errors, right-click on it, select *Properties*, the +*Java Build Path*, then click on the *Projects* tab. +4. Click on the *Add...* button and select `fdroidclient/` +5. Right-click on the **fdroid-test** project, then *Run As...* -> *Android JUnit Test* + + License ------- From 97e3bac98eff792f28e551798e3d38ec025ad9da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 14 Feb 2014 09:10:58 +0100 Subject: [PATCH 095/282] Bump gradle plugin to 0.8 (gradle 1.10) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 731e88b8f..c122233be 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.7.+' + classpath 'com.android.tools.build:gradle:0.8.+' } } From 51a02fe40fe3195234fc08e0cb2929588a3a8c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 14 Feb 2014 09:12:52 +0100 Subject: [PATCH 096/282] Update libraries --- extern/Universal-Image-Loader | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/Universal-Image-Loader b/extern/Universal-Image-Loader index f49a5a0e5..af4873eb8 160000 --- a/extern/Universal-Image-Loader +++ b/extern/Universal-Image-Loader @@ -1 +1 @@ -Subproject commit f49a5a0e50d5b817c1c531abed3b7945f8a7ff42 +Subproject commit af4873eb8b76237c80e3aee2c62fff437a2fd7d0 From cf1519f792d0c0a80c1dc3a298b174a5556b9b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 14 Feb 2014 09:16:18 +0100 Subject: [PATCH 097/282] Add eclipse files to gitignore --- .gitignore | 1 + ant-prepare.sh | 2 +- test/.gitignore | 2 ++ test/.project | 33 ------------------ test/build.xml | 92 ------------------------------------------------- 5 files changed, 4 insertions(+), 126 deletions(-) delete mode 100644 test/.project delete mode 100644 test/build.xml diff --git a/.gitignore b/.gitignore index 4f54aeced..8345f35d2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ /build.xml *~ /.idea/ +/.project /*.iml out diff --git a/ant-prepare.sh b/ant-prepare.sh index 5463a67d5..51d53ace7 100755 --- a/ant-prepare.sh +++ b/ant-prepare.sh @@ -7,4 +7,4 @@ android update project --path . --name F-Droid # technically optional, needed for the tests cd test -android update test-project --path ./ --main ../ +android update test-project --path . --main ../ --name test diff --git a/test/.gitignore b/test/.gitignore index 24e19e4db..8345f35d2 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -4,7 +4,9 @@ /gen/ /build/ /.gradle/ +/build.xml *~ /.idea/ +/.project /*.iml out diff --git a/test/.project b/test/.project deleted file mode 100644 index 0c2b67ff9..000000000 --- a/test/.project +++ /dev/null @@ -1,33 +0,0 @@ - - - fdroid-test - - - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - - diff --git a/test/build.xml b/test/build.xml deleted file mode 100644 index acf244066..000000000 --- a/test/build.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From c99cf93c8bded761c59370a9ac4719cf3de6a329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 14 Feb 2014 09:18:21 +0100 Subject: [PATCH 098/282] Test projects can't have --name it seems --- ant-prepare.sh | 2 +- test/ant.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ant-prepare.sh b/ant-prepare.sh index 51d53ace7..04599ffa3 100755 --- a/ant-prepare.sh +++ b/ant-prepare.sh @@ -7,4 +7,4 @@ android update project --path . --name F-Droid # technically optional, needed for the tests cd test -android update test-project --path . --main ../ --name test +android update test-project --path . --main .. diff --git a/test/ant.properties b/test/ant.properties index 836edf047..16244024c 100644 --- a/test/ant.properties +++ b/test/ant.properties @@ -15,4 +15,4 @@ # 'key.alias' for the name of the key to use. # The password will be asked during the build when you use the 'release' target. -tested.project.dir=../ +tested.project.dir=.. From 967174b549e4fb8e2934c20f634e667691874457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 14 Feb 2014 10:34:36 +0100 Subject: [PATCH 099/282] Finally get UIL working as a gradle library The problem were the dashes in the path 'extern/Universal-Image-Loader' --- .gitmodules | 2 +- build.gradle | 2 +- extern/{Universal-Image-Loader => UniversalImageLoader} | 0 settings.gradle | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename extern/{Universal-Image-Loader => UniversalImageLoader} (100%) diff --git a/.gitmodules b/.gitmodules index c242b4487..b54d0200a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,5 +1,5 @@ [submodule "extern/Universal-Image-Loader"] - path = extern/Universal-Image-Loader + path = extern/UniversalImageLoader url = https://github.com/nostra13/Android-Universal-Image-Loader ignore = dirty [submodule "extern/MemorizingTrustManager"] diff --git a/build.gradle b/build.gradle index c122233be..cc191978a 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ android { sourceSets { main { manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = ['src', 'extern/Universal-Image-Loader/library/src'] + java.srcDirs = ['src'] resources.srcDirs = ['src'] aidl.srcDirs = ['src'] renderscript.srcDirs = ['src'] diff --git a/extern/Universal-Image-Loader b/extern/UniversalImageLoader similarity index 100% rename from extern/Universal-Image-Loader rename to extern/UniversalImageLoader diff --git a/settings.gradle b/settings.gradle index d6ec4ceb0..719875856 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':extern:AndroidPinning' +include ':extern:AndroidPinning', ':extern:UniversalImageLoader' From fffae79c24b4e26bf5729f969bd260e39bf17a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 14 Feb 2014 10:51:30 +0100 Subject: [PATCH 100/282] Add MemorizingTrustManager as gradle library too --- build.gradle | 1 + settings.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cc191978a..677dd9faf 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ apply plugin: 'android' dependencies { compile files('libs/android-support-v4.jar') compile project(':extern:AndroidPinning') + compile project(':extern:MemorizingTrustManager') } android { diff --git a/settings.gradle b/settings.gradle index 719875856..9a7be7751 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':extern:AndroidPinning', ':extern:UniversalImageLoader' +include ':extern:AndroidPinning', ':extern:UniversalImageLoader', ':extern:MemorizingTrustManager' From 1e9c6ccf2ebdf07e1c5581760a9a09bdb2eb340a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 14 Feb 2014 12:17:07 +0100 Subject: [PATCH 101/282] Finally give up on UIL and set up the gradle project ourselves --- build.gradle | 20 ++++++++++++++++++++ settings.gradle | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 677dd9faf..489dee087 100644 --- a/build.gradle +++ b/build.gradle @@ -12,9 +12,29 @@ apply plugin: 'android' dependencies { compile files('libs/android-support-v4.jar') compile project(':extern:AndroidPinning') + compile project(':extern:UniversalImageLoader:library') compile project(':extern:MemorizingTrustManager') } +project(':extern:UniversalImageLoader:library') { + apply plugin: 'android-library' + + android { + compileSdkVersion 16 + buildToolsVersion '19.0.1' + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src'] + resources.srcDirs = ['src'] + aidl.srcDirs = ['src'] + renderscript.srcDirs = ['src'] + } + } + } +} + android { compileSdkVersion 19 buildToolsVersion "19.0.1" diff --git a/settings.gradle b/settings.gradle index 9a7be7751..66e6ee88a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':extern:AndroidPinning', ':extern:UniversalImageLoader', ':extern:MemorizingTrustManager' +include ':extern:AndroidPinning', ':extern:UniversalImageLoader:library', ':extern:MemorizingTrustManager' From b709f9e17a1127e9569ea804cde91abcb109341b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 14 Feb 2014 21:26:04 +0100 Subject: [PATCH 102/282] Update AndroidPinning --- extern/AndroidPinning | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/AndroidPinning b/extern/AndroidPinning index 5cfc3f51d..ce84a19e7 160000 --- a/extern/AndroidPinning +++ b/extern/AndroidPinning @@ -1 +1 @@ -Subproject commit 5cfc3f51dc9437577c1ded7cb5bca48b95eea131 +Subproject commit ce84a19e753bbcc3304525f763edb7d7f3b62429 From 0c06b67f3dd93a73c762e53d2427c87517260d6c Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 14 Feb 2014 20:46:12 -0500 Subject: [PATCH 103/282] do not require WiFi in a device, any internet access will work Setting android.permission.ACCESS_WIFI_STATE automatically sets up uses-feature to require wifi. Therefore, we have to manually say that wifi is not actually required. --- AndroidManifest.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index d983b6dda..97fa4d86a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -17,6 +17,9 @@ android:smallScreens="true" android:xlargeScreens="true" /> + From c0cd0d33bfdbec623cf0c31a83584d409106b141 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 14 Feb 2014 22:40:48 -0500 Subject: [PATCH 104/282] send the FDroid.apk via bluetooth on devices that support it This is another easy method to send FDroid to a device that doesn't have it yet. Unfortunately, stock Android blocks the receiving of APKs, but many ROMs and even some Samsung devices do not have this block. You can find the lengthy backstory on this work here: https://dev.guardianproject.info/issues/2084 --- AndroidManifest.xml | 4 ++ res/values/strings.xml | 3 ++ src/org/fdroid/fdroid/FDroid.java | 77 ++++++++++++++++++++++++++++++- 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 97fa4d86a..e39145c3a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -26,10 +26,14 @@ + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 0895ad430..680cd375d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -76,6 +76,8 @@ Getting application from NFC is not enabled! Go to NFC Settings… + No Bluetooth send method found, choose one! + Choose Bluetooth send method Repository address Fingerprint (optional) @@ -92,6 +94,7 @@ Update Repos Repositories + Bluetooth FDroid.apk… Preferences About Search diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index eaf9db0ac..9156177b9 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -20,14 +20,18 @@ package org.fdroid.fdroid; import android.annotation.TargetApi; +import android.app.Activity; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.NotificationManager; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothManager; import android.content.*; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.database.ContentObserver; import android.net.Uri; @@ -44,6 +48,7 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.TextView; +import android.widget.Toast; import org.fdroid.fdroid.compat.TabManager; import org.fdroid.fdroid.data.AppProvider; @@ -54,6 +59,7 @@ public class FDroid extends FragmentActivity { public static final int REQUEST_APPDETAILS = 0; public static final int REQUEST_MANAGEREPOS = 1; public static final int REQUEST_PREFS = 2; + public static final int REQUEST_ENABLE_BLUETOOTH = 3; public static final String EXTRA_TAB_UPDATE = "extraTab"; @@ -61,6 +67,10 @@ public class FDroid extends FragmentActivity { private static final int PREFERENCES = Menu.FIRST + 1; private static final int ABOUT = Menu.FIRST + 2; private static final int SEARCH = Menu.FIRST + 3; + private static final int BLUETOOTH_APK = Menu.FIRST + 4; + + /* request codes for Bluetooth flows */ + private BluetoothAdapter mBluetoothAdapter = null; private ViewPager viewPager; @@ -101,6 +111,18 @@ public class FDroid extends FragmentActivity { Uri uri = AppProvider.getContentUri(); getContentResolver().registerContentObserver(uri, true, new AppObserver()); + + getBluetoothAdapter(); + } + + @TargetApi(18) + private void getBluetoothAdapter() { + // to use the new, recommended way of getting the adapter + // http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html + if (Build.VERSION.SDK_INT < 18) + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + else + mBluetoothAdapter = ((BluetoothManager) getSystemService(BLUETOOTH_SERVICE)).getAdapter(); } @Override @@ -125,6 +147,8 @@ public class FDroid extends FragmentActivity { android.R.drawable.ic_menu_agenda); MenuItem search = menu.add(Menu.NONE, SEARCH, 3, R.string.menu_search).setIcon( android.R.drawable.ic_menu_search); + if (mBluetoothAdapter != null) // ignore on devices without Bluetooth + menu.add(Menu.NONE, BLUETOOTH_APK, 3, R.string.menu_send_apk_bt); menu.add(Menu.NONE, PREFERENCES, 4, R.string.menu_preferences).setIcon( android.R.drawable.ic_menu_preferences); menu.add(Menu.NONE, ABOUT, 5, R.string.menu_about).setIcon( @@ -152,6 +176,17 @@ public class FDroid extends FragmentActivity { onSearchRequested(); return true; + case BLUETOOTH_APK: + /* + * If Bluetooth has not been enabled/turned on, then + * enabling device discoverability will automatically enable Bluetooth + */ + Intent discoverBt = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); + discoverBt.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 121); + startActivityForResult(discoverBt, REQUEST_ENABLE_BLUETOOTH); + // if this is successful, the Bluetooth transfer is started + return true; + case ABOUT: View view = null; if (Build.VERSION.SDK_INT >= 11) { @@ -259,7 +294,47 @@ public class FDroid extends FragmentActivity { overridePendingTransition(0, 0); startActivity(intent); } - + break; + case REQUEST_ENABLE_BLUETOOTH: + if (resultCode == Activity.RESULT_CANCELED) + break; + String packageName = null; + String className = null; + boolean found = false; + Intent sendBt = null; + try { + PackageManager pm = getPackageManager(); + ApplicationInfo appInfo = pm.getApplicationInfo("org.fdroid.fdroid", + PackageManager.GET_META_DATA); + sendBt = new Intent(Intent.ACTION_SEND); + // The APK type is blocked by stock Android, so use zip + // sendBt.setType("application/vnd.android.package-archive"); + sendBt.setType("application/zip"); + sendBt.putExtra(Intent.EXTRA_STREAM, + Uri.parse("file://" + appInfo.publicSourceDir)); + // not all devices have the same Bluetooth Activities, so + // let's find it + for (ResolveInfo info : pm.queryIntentActivities(sendBt, 0)) { + packageName = info.activityInfo.packageName; + if (packageName.equals("com.android.bluetooth") + || packageName.equals("com.mediatek.bluetooth")) { + className = info.activityInfo.name; + found = true; + break; + } + } + } catch (NameNotFoundException e1) { + e1.printStackTrace(); + found = false; + } + if (!found) { + Toast.makeText(this, R.string.bluetooth_activity_not_found, + Toast.LENGTH_SHORT).show(); + startActivity(Intent.createChooser(sendBt, getString(R.string.choose_bt_send))); + } else { + sendBt.setClassName(packageName, className); + startActivity(sendBt); + } break; } } From 6d111c6e7d09fa1e6e70576e5e4eee079eee0848 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 14 Feb 2014 22:41:08 -0500 Subject: [PATCH 105/282] document NFC/Bluetooth send methods in CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1af5edcf4..edbbf8abe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ### Upcoming release +* Always remember the selected category in the list of apps + +* Send FDroid via Bluetooth to any device that supports receiving APKs via + Bluetooth (stock Android blocks APKs, most ROMs allow them) + +* NFC support: beam repo configs from the repo detail view (Android 4.0+), + beam the FDroid.apk from FDroid's main screen (Android 4.1+) + * Support for repositories using self-signed HTTPS certificates through Trust-on-first-use popup From 739ecfdea39b348cef5f3b2323792b2a5c3eff69 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 14 Feb 2014 23:07:49 -0500 Subject: [PATCH 106/282] commit Eclipse project files to make it easier for others to start Having the pre-configured Eclipse files in git will make it easier for other people to work with FDroid in Eclipse, and should not affect anything else. The key files are .classpath and .project. The .settings/ folder is for user-specific settings, so its ignored. --- .classpath | 12 ++++++++++++ .gitignore | 3 +-- test/.classpath | 10 ++++++++++ test/.gitignore | 3 +-- 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 .classpath create mode 100644 test/.classpath diff --git a/.classpath b/.classpath new file mode 100644 index 000000000..37a11b83e --- /dev/null +++ b/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore index 8345f35d2..22273c3f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /local.properties -/.classpath /bin/ /gen/ /build/ @@ -7,6 +6,6 @@ /build.xml *~ /.idea/ -/.project /*.iml out +/.settings/ diff --git a/test/.classpath b/test/.classpath new file mode 100644 index 000000000..d585386c3 --- /dev/null +++ b/test/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/test/.gitignore b/test/.gitignore index 8345f35d2..22273c3f4 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -1,5 +1,4 @@ /local.properties -/.classpath /bin/ /gen/ /build/ @@ -7,6 +6,6 @@ /build.xml *~ /.idea/ -/.project /*.iml out +/.settings/ From 3a1b814603fafbadcac856c43f37ca9d1c0eefc2 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 14 Feb 2014 23:17:48 -0500 Subject: [PATCH 107/282] fix RO translation's formats, based on lint warning Inconsistent formatting types for argument #1 in format string searchres_napps ('%s'): Found both 'd' and 's' (in values/strings.xml) This lint check ensures the following: (1) If there are multiple translations of the format string, then all translations use the same type for the same numbered arguments (2) The usage of the format string in Java consistent with the format string, meaning that the parameter types passed to String.format matches those in the format string. Sa gasit o aplicatie potrivita cu %s\' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --- res/values-ro/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml index 40c1a4fe3..f0f0db7ae 100644 --- a/res/values-ro/strings.xml +++ b/res/values-ro/strings.xml @@ -1,8 +1,8 @@ - Sa gasit o aplicatie potrivita cu %s\' - Sa gasit o aplicatie potrivita cu %s\' - Nu exita aplicatii potrivite cu %s\': + Sa gasit %1$d aplicații potrivita cu \'%2$s\' + Sa gasit o aplicatie potrivita cu \'%s\' + Nu exita aplicatii potrivite cu \'%s\': Versiune Istoric aplicatii descarcate Noutati From 6b2b759a16f665d8c459cbaa1adc7fce167ddaf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 15 Feb 2014 11:18:58 +0100 Subject: [PATCH 108/282] Whoops, forgot to also change the ant setup --- ant-prepare.sh | 2 +- project.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ant-prepare.sh b/ant-prepare.sh index 04599ffa3..299d005e9 100755 --- a/ant-prepare.sh +++ b/ant-prepare.sh @@ -1,6 +1,6 @@ #!/bin/bash -ex -android update lib-project --path extern/Universal-Image-Loader/library +android update lib-project --path extern/UniversalImageLoader/library android update lib-project --path extern/AndroidPinning android update lib-project --path extern/MemorizingTrustManager android update project --path . --name F-Droid diff --git a/project.properties b/project.properties index 6da907dd2..d6e5f072f 100644 --- a/project.properties +++ b/project.properties @@ -2,6 +2,6 @@ proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project. target=android-19 -android.library.reference.1=extern/Universal-Image-Loader/library +android.library.reference.1=extern/UniversalImageLoader/library android.library.reference.2=extern/MemorizingTrustManager android.library.reference.3=extern/AndroidPinning From 4e26c77327a8618d072882fbf49a792091eb5243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 15 Feb 2014 11:23:30 +0100 Subject: [PATCH 109/282] Don't crash on startup if NFC is not available --- src/org/fdroid/fdroid/FDroid.java | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index 9156177b9..e280610e4 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -423,17 +423,19 @@ public class FDroid extends FragmentActivity { private void setupAndroidBeam() { PackageManager pm = getPackageManager(); NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); - ApplicationInfo appInfo; - try { - appInfo = pm.getApplicationInfo("org.fdroid.fdroid", - PackageManager.GET_META_DATA); - // TODO can we send the repo here also, as a file? - Uri uris[] = { - Uri.parse("file://" + appInfo.publicSourceDir), - }; - nfcAdapter.setBeamPushUris(uris, this); - } catch (NameNotFoundException e1) { - e1.printStackTrace(); + if (nfcAdapter != null) { + ApplicationInfo appInfo; + try { + appInfo = pm.getApplicationInfo("org.fdroid.fdroid", + PackageManager.GET_META_DATA); + // TODO can we send the repo here also, as a file? + Uri uris[] = { + Uri.parse("file://" + appInfo.publicSourceDir), + }; + nfcAdapter.setBeamPushUris(uris, this); + } catch (NameNotFoundException e1) { + e1.printStackTrace(); + } } } } From 901545d404823798e40c8a8a92922fabc35d0d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 15 Feb 2014 11:49:29 +0100 Subject: [PATCH 110/282] Don't recreate main activity when returning to it Huge improvements! Amongst them: * Pressing Up is just as fast as pressing Back * Like Back, it keeps the scroll position and everything * Now FDroid behaves like the other activities that an user may navigate up to --- AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e39145c3a..fa6e046f1 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -63,6 +63,7 @@ From feec3b1c518aebc962d4ae26e576a170a4918aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 15 Feb 2014 12:31:15 +0100 Subject: [PATCH 111/282] Make all the gradle libs use the same latest build-tools version This makes it completely build from source properly on a standard setup --- build.gradle | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 489dee087..405a07503 100644 --- a/build.gradle +++ b/build.gradle @@ -17,22 +17,30 @@ dependencies { } project(':extern:UniversalImageLoader:library') { - apply plugin: 'android-library' + apply plugin: 'android-library' - android { - compileSdkVersion 16 - buildToolsVersion '19.0.1' + android { + compileSdkVersion 16 + buildToolsVersion '19.0.1' - sourceSets { - main { - manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = ['src'] - resources.srcDirs = ['src'] - aidl.srcDirs = ['src'] - renderscript.srcDirs = ['src'] - } - } - } + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src'] + resources.srcDirs = ['src'] + aidl.srcDirs = ['src'] + renderscript.srcDirs = ['src'] + } + } + } +} + +project(':extern:AndroidPinning') { + android { buildToolsVersion '19.0.1' } +} + +project(':extern:MemorizingTrustManager') { + android { buildToolsVersion '19.0.1' } } android { From 41b5797307b2c46b25108a89665458071bbdfa97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 16 Feb 2014 02:48:22 +0100 Subject: [PATCH 112/282] Automatic tab fixing --- .../src/mock/MockContextSwappableComponents.java | 2 +- test/src/org/fdroid/fdroid/AppProviderTest.java | 16 ++++++++-------- .../org/fdroid/fdroid/FDroidProviderTest.java | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/test/src/mock/MockContextSwappableComponents.java b/test/src/mock/MockContextSwappableComponents.java index 750adbed6..9cb09f466 100644 --- a/test/src/mock/MockContextSwappableComponents.java +++ b/test/src/mock/MockContextSwappableComponents.java @@ -38,6 +38,6 @@ public class MockContextSwappableComponents extends MockContext { @Override public MockContentResolver getContentResolver() { - return contentResolver; + return contentResolver; } } diff --git a/test/src/org/fdroid/fdroid/AppProviderTest.java b/test/src/org/fdroid/fdroid/AppProviderTest.java index 3252fd829..875b84c92 100644 --- a/test/src/org/fdroid/fdroid/AppProviderTest.java +++ b/test/src/org/fdroid/fdroid/AppProviderTest.java @@ -24,7 +24,7 @@ public class AppProviderTest extends FDroidProviderTest { public void setUp() throws Exception { super.setUp(); getSwappableContext().setResources(new MockCategoryResources()); - getSwappableContext().setContentResolver(getMockContentResolver()); + getSwappableContext().setContentResolver(getMockContentResolver()); } protected String[] getMinimalProjection() { @@ -135,13 +135,13 @@ public class AppProviderTest extends FDroidProviderTest { List categories = AppProvider.Helper.categories(getMockContext()); String[] expected = new String[] { - getMockContext().getResources().getString(R.string.category_whatsnew), - getMockContext().getResources().getString(R.string.category_recentlyupdated), - getMockContext().getResources().getString(R.string.category_all), - "Animal", - "Mineral", - "Vegetable" - }; + getMockContext().getResources().getString(R.string.category_whatsnew), + getMockContext().getResources().getString(R.string.category_recentlyupdated), + getMockContext().getResources().getString(R.string.category_all), + "Animal", + "Mineral", + "Vegetable" + }; assertContainsOnly(categories, expected); } diff --git a/test/src/org/fdroid/fdroid/FDroidProviderTest.java b/test/src/org/fdroid/fdroid/FDroidProviderTest.java index 776202ba0..23ba41742 100644 --- a/test/src/org/fdroid/fdroid/FDroidProviderTest.java +++ b/test/src/org/fdroid/fdroid/FDroidProviderTest.java @@ -53,10 +53,10 @@ public abstract class FDroidProviderTest extends Provi protected void assertInvalidUri(Uri uri) { try { - // Use getProvdider instead of getContentResolver, because the mock - // content resolver wont result in the provider we are testing, and - // hence we don't get to see how our provider responds to invalid - // uris. + // Use getProvdider instead of getContentResolver, because the mock + // content resolver wont result in the provider we are testing, and + // hence we don't get to see how our provider responds to invalid + // uris. getProvider().query(uri, getMinimalProjection(), null, null, null); fail(); } catch (UnsupportedOperationException e) {} From 43f8ea08140e7cd072fbe83a139d528d75c57fd5 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 11 Feb 2014 07:34:01 +1100 Subject: [PATCH 113/282] Added category tests. This will be useful when somebody wants to move categories from a comma separated string in the app table, to a separate table all together. --- test/src/mock/MockCategoryResources.java | 21 +++ .../mock/MockContextSwappableComponents.java | 13 +- .../org/fdroid/fdroid/AppProviderTest.java | 160 +++++++++++++++--- .../org/fdroid/fdroid/FDroidProviderTest.java | 6 +- test/src/org/fdroid/fdroid/TestUtils.java | 30 ++++ 5 files changed, 208 insertions(+), 22 deletions(-) create mode 100644 test/src/mock/MockCategoryResources.java create mode 100644 test/src/org/fdroid/fdroid/TestUtils.java diff --git a/test/src/mock/MockCategoryResources.java b/test/src/mock/MockCategoryResources.java new file mode 100644 index 000000000..669ddd1d0 --- /dev/null +++ b/test/src/mock/MockCategoryResources.java @@ -0,0 +1,21 @@ +package mock; + +import android.test.mock.*; +import org.fdroid.fdroid.*; + +public class MockCategoryResources extends MockResources { + + @Override + public String getString(int id) { + if (id == R.string.category_all) { + return "All"; + } else if (id == R.string.category_recentlyupdated) { + return "Recently Updated"; + } else if (id == R.string.category_whatsnew) { + return "Whats New"; + } else { + return ""; + } +} + +} diff --git a/test/src/mock/MockContextSwappableComponents.java b/test/src/mock/MockContextSwappableComponents.java index 7fcbbf971..750adbed6 100644 --- a/test/src/mock/MockContextSwappableComponents.java +++ b/test/src/mock/MockContextSwappableComponents.java @@ -2,13 +2,14 @@ package mock; import android.content.pm.PackageManager; import android.content.res.Resources; -import android.test.mock.MockContext; +import android.test.mock.*; public class MockContextSwappableComponents extends MockContext { private PackageManager packageManager; private Resources resources; + private MockContentResolver contentResolver; public MockContextSwappableComponents setPackageManager(PackageManager pm) { packageManager = pm; @@ -20,6 +21,11 @@ public class MockContextSwappableComponents extends MockContext { return this; } + public MockContextSwappableComponents setContentResolver(MockContentResolver contentResolver) { + this.contentResolver = contentResolver; + return this; + } + @Override public PackageManager getPackageManager() { return packageManager; @@ -29,4 +35,9 @@ public class MockContextSwappableComponents extends MockContext { public Resources getResources() { return resources; } + + @Override + public MockContentResolver getContentResolver() { + return contentResolver; + } } diff --git a/test/src/org/fdroid/fdroid/AppProviderTest.java b/test/src/org/fdroid/fdroid/AppProviderTest.java index c1ccb7b9c..3252fd829 100644 --- a/test/src/org/fdroid/fdroid/AppProviderTest.java +++ b/test/src/org/fdroid/fdroid/AppProviderTest.java @@ -1,18 +1,18 @@ package org.fdroid.fdroid; import android.content.ContentValues; -import android.content.pm.PackageInfo; import android.database.Cursor; import android.net.Uri; -import android.provider.ContactsContract; +import junit.framework.AssertionFailedError; +import mock.MockCategoryResources; import mock.MockInstallablePackageManager; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppProvider; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Map; public class AppProviderTest extends FDroidProviderTest { @@ -20,6 +20,13 @@ public class AppProviderTest extends FDroidProviderTest { super(AppProvider.class, AppProvider.getAuthority()); } + @Override + public void setUp() throws Exception { + super.setUp(); + getSwappableContext().setResources(new MockCategoryResources()); + getSwappableContext().setContentResolver(getMockContentResolver()); + } + protected String[] getMinimalProjection() { return new String[] { AppProvider.DataColumns.APP_ID, @@ -53,6 +60,12 @@ public class AppProviderTest extends FDroidProviderTest { assertNotNull(cursor); } + private void insertApps(int count) { + for (int i = 0; i < count; i ++) { + insertApp("com.example.test." + i, "Test app " + i); + } + } + public void testInstalled() { Utils.clearInstalledApksCache(); @@ -60,9 +73,7 @@ public class AppProviderTest extends FDroidProviderTest { MockInstallablePackageManager pm = new MockInstallablePackageManager(); getSwappableContext().setPackageManager(pm); - for (int i = 0; i < 100; i ++) { - insertApp("com.example.test." + i, "Test app " + i); - } + insertApps(100); assertAppCount(100, AppProvider.getContentUri()); assertAppCount(0, AppProvider.getInstalledUri()); @@ -75,7 +86,7 @@ public class AppProviderTest extends FDroidProviderTest { } private void assertAppCount(int expectedCount, Uri uri) { - Cursor cursor = getProvider().query(uri, getMinimalProjection(), null, null, null); + Cursor cursor = getMockContentResolver().query(uri, getMinimalProjection(), null, null, null); assertNotNull(cursor); assertEquals(expectedCount, cursor.getCount()); } @@ -114,25 +125,134 @@ public class AppProviderTest extends FDroidProviderTest { } private Cursor queryAllApps() { - return getProvider().query(AppProvider.getContentUri(), getMinimalProjection(), null, null, null); + return getMockContentResolver().query(AppProvider.getContentUri(), getMinimalProjection(), null, null, null); + } + + public void testCategoriesSingle() { + insertAppWithCategory("com.dog", "Dog", "Animal"); + insertAppWithCategory("com.rock", "Rock", "Mineral"); + insertAppWithCategory("com.banana", "Banana", "Vegetable"); + + List categories = AppProvider.Helper.categories(getMockContext()); + String[] expected = new String[] { + getMockContext().getResources().getString(R.string.category_whatsnew), + getMockContext().getResources().getString(R.string.category_recentlyupdated), + getMockContext().getResources().getString(R.string.category_all), + "Animal", + "Mineral", + "Vegetable" + }; + assertContainsOnly(categories, expected); + } + + public void testCategoriesMultiple() { + insertAppWithCategory("com.rock.dog", "Rock-Dog", "Mineral,Animal"); + insertAppWithCategory("com.dog.rock.apple", "Dog-Rock-Apple", "Animal,Mineral,Vegetable"); + insertAppWithCategory("com.banana.apple", "Banana", "Vegetable,Vegetable"); + + List categories = AppProvider.Helper.categories(getMockContext()); + String[] expected = new String[] { + getMockContext().getResources().getString(R.string.category_whatsnew), + getMockContext().getResources().getString(R.string.category_recentlyupdated), + getMockContext().getResources().getString(R.string.category_all), + + "Animal", + "Mineral", + "Vegetable" + }; + assertContainsOnly(categories, expected); + + insertAppWithCategory("com.example.game", "Game", + "Running,Shooting,Jumping,Bleh,Sneh,Pleh,Blah,Test category," + + "The quick brown fox jumps over the lazy dog,With apostrophe's"); + + List categoriesLonger = AppProvider.Helper.categories(getMockContext()); + String[] expectedLonger = new String[] { + getMockContext().getResources().getString(R.string.category_whatsnew), + getMockContext().getResources().getString(R.string.category_recentlyupdated), + getMockContext().getResources().getString(R.string.category_all), + + "Animal", + "Mineral", + "Vegetable", + + "Running", + "Shooting", + "Jumping", + "Bleh", + "Sneh", + "Pleh", + "Blah", + "Test category", + "The quick brown fox jumps over the lazy dog", + "With apostrophe's" + }; + + assertContainsOnly(categoriesLonger, expectedLonger); } private void insertApp(String id, String name) { - ContentValues values = new ContentValues(2); - values.put(AppProvider.DataColumns.APP_ID, id); - values.put(AppProvider.DataColumns.NAME, name); + insertApp(id, name, new ContentValues()); + } - // Required fields (NOT NULL in the database). - values.put(AppProvider.DataColumns.SUMMARY, "test summary"); - values.put(AppProvider.DataColumns.DESCRIPTION, "test description"); - values.put(AppProvider.DataColumns.LICENSE, "GPL?"); - values.put(AppProvider.DataColumns.IS_COMPATIBLE, 1); - values.put(AppProvider.DataColumns.IGNORE_ALLUPDATES, 0); - values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, 0); + private void insertAppWithCategory(String id, String name, + String categories) { + ContentValues values = new ContentValues(1); + values.put(AppProvider.DataColumns.CATEGORIES, categories); + insertApp(id, name, values); + } - Uri uri = AppProvider.getContentUri(); + private void insertApp(String id, String name, + ContentValues additionalValues) { + TestUtils.insertApp(getMockContentResolver(), id, name, additionalValues); + } - getProvider().insert(uri, values); + private void assertContainsOnly(List actualList, T[] expectedContains) { + List containsList = new ArrayList(expectedContains.length); + Collections.addAll(containsList, expectedContains); + assertContainsOnly(actualList, containsList); + } + + private String listToString(List list) { + String string = "["; + for (int i = 0; i < list.size(); i ++) { + if (i > 0) { + string += ", "; + } + string += list.get(i); + } + string += "]"; + return string; + } + + private void assertContainsOnly(List actualList, List expectedContains) { + if (actualList.size() != expectedContains.size()) { + String message = + "List sizes don't match.\n" + + "Expected: " + + listToString(expectedContains) + "\n" + + "Actual: " + + listToString(actualList); + throw new AssertionFailedError(message); + } + for (T required : expectedContains) { + boolean containsRequired = false; + for (T itemInList : actualList) { + if (required.equals(itemInList)) { + containsRequired = true; + break; + } + } + if (!containsRequired) { + String message = + "List doesn't contain \"" + required + "\".\n" + + "Expected: " + + listToString(expectedContains) + "\n" + + "Actual: " + + listToString(actualList); + throw new AssertionFailedError(message); + } + } } } diff --git a/test/src/org/fdroid/fdroid/FDroidProviderTest.java b/test/src/org/fdroid/fdroid/FDroidProviderTest.java index eaaa84e57..776202ba0 100644 --- a/test/src/org/fdroid/fdroid/FDroidProviderTest.java +++ b/test/src/org/fdroid/fdroid/FDroidProviderTest.java @@ -53,13 +53,17 @@ public abstract class FDroidProviderTest extends Provi protected void assertInvalidUri(Uri uri) { try { + // Use getProvdider instead of getContentResolver, because the mock + // content resolver wont result in the provider we are testing, and + // hence we don't get to see how our provider responds to invalid + // uris. getProvider().query(uri, getMinimalProjection(), null, null, null); fail(); } catch (UnsupportedOperationException e) {} } protected void assertValidUri(Uri uri) { - Cursor cursor = getProvider().query(uri, getMinimalProjection(), null, null, null); + Cursor cursor = getMockContentResolver().query(uri, getMinimalProjection(), null, null, null); assertNotNull(cursor); } diff --git a/test/src/org/fdroid/fdroid/TestUtils.java b/test/src/org/fdroid/fdroid/TestUtils.java new file mode 100644 index 000000000..a01f85460 --- /dev/null +++ b/test/src/org/fdroid/fdroid/TestUtils.java @@ -0,0 +1,30 @@ +package org.fdroid.fdroid; + +import android.content.*; +import android.net.Uri; +import org.fdroid.fdroid.data.AppProvider; + +public class TestUtils { + + public static void insertApp(ContentResolver resolver, String id, String name, ContentValues additionalValues) { + + ContentValues values = new ContentValues(); + values.put(AppProvider.DataColumns.APP_ID, id); + values.put(AppProvider.DataColumns.NAME, name); + + // Required fields (NOT NULL in the database). + values.put(AppProvider.DataColumns.SUMMARY, "test summary"); + values.put(AppProvider.DataColumns.DESCRIPTION, "test description"); + values.put(AppProvider.DataColumns.LICENSE, "GPL?"); + values.put(AppProvider.DataColumns.IS_COMPATIBLE, 1); + values.put(AppProvider.DataColumns.IGNORE_ALLUPDATES, 0); + values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, 0); + + values.putAll(additionalValues); + + Uri uri = AppProvider.getContentUri(); + + resolver.insert(uri, values); + } + +} From 8473627370631ea02377c5641f0de1941e3bae63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 17 Feb 2014 10:34:28 +0100 Subject: [PATCH 114/282] Update changelog with my recent changes --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edbbf8abe..790b0b3b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,12 @@ beam the FDroid.apk from FDroid's main screen (Android 4.1+) * Support for repositories using self-signed HTTPS certificates through - Trust-on-first-use popup + a Trust-on-first-use popup * Support for TLS Subject-Public-Key-Identifier pinning +* Various fixes to layout issues introduced in 0.58 + ### 0.58 (2014-01-11) * Download icons with a resolution that matches the device's screen density, From b222887745786999e9d6864e1b46a7fd206314b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 17 Feb 2014 10:42:19 +0100 Subject: [PATCH 115/282] Start TODO to be emptied before the upcoming stable release --- TODO-before-release.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 TODO-before-release.md diff --git a/TODO-before-release.md b/TODO-before-release.md new file mode 100644 index 000000000..b755ba856 --- /dev/null +++ b/TODO-before-release.md @@ -0,0 +1,14 @@ +These issues are a must-fix before the next stable release: + +* Move Ignore settings into separate table to not overwrite them upon repo + update + +* Right after updating a repo, "Recently Updated" shows the apps correctly but + the new apks don't show up on App Details until the whole app is restarted + (or until the repos are wiped and re-downloaded) + +Other minor issues: + +* Make the bluetooth option prettier. Options: + - Move it into submenu (like "Share F-Droid" -> "Bluetooth/Mail/NFC/...") + - Remove ellipsis from menu option string From 799be522249914048774c734fb6609de492b4981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 17 Feb 2014 16:09:57 +0100 Subject: [PATCH 116/282] Fix regression: Don't count apps which we don't want to update --- src/org/fdroid/fdroid/UpdateService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index 4741aa51a..ac7689b9a 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -304,8 +304,8 @@ public class UpdateService extends IntentService implements ProgressListener { if (success && changes && prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, false)) { int updateCount = 0; for (App app : appsToUpdate.values()) { - if (app.hasUpdates(this)) { - updateCount ++; + if (app.canAndWantToUpdate(this)) { + updateCount++; } } From 85f3232de094ec5806ecb18f3adcd8cf6e46a1c1 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 18 Feb 2014 02:52:02 +1100 Subject: [PATCH 117/282] Started implementing ApkProvider tests. Refactored a couple of common things from AppProviderTest to either FDroidProviderTest (baseclass) or TestUtils (static methods) where relevant. --- src/org/fdroid/fdroid/data/ApkProvider.java | 5 + .../org/fdroid/fdroid/ApkProviderTest.java | 131 ++++++++++++++++++ .../org/fdroid/fdroid/AppProviderTest.java | 69 +-------- .../org/fdroid/fdroid/FDroidProviderTest.java | 12 ++ test/src/org/fdroid/fdroid/TestUtils.java | 76 ++++++++++ test/src/org/fdroid/fdroid/mock/MockApk.java | 12 ++ test/src/org/fdroid/fdroid/mock/MockApp.java | 16 +++ 7 files changed, 258 insertions(+), 63 deletions(-) create mode 100644 test/src/org/fdroid/fdroid/ApkProviderTest.java create mode 100644 test/src/org/fdroid/fdroid/mock/MockApk.java create mode 100644 test/src/org/fdroid/fdroid/mock/MockApp.java diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java index a4b16b39c..b957b2093 100644 --- a/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -366,6 +366,11 @@ public class ApkProvider extends FDroidProvider { String[] apkDetails = apkKeys.split(","); String[] args = new String[apkDetails.length * 2]; StringBuilder sb = new StringBuilder(); + if (apkDetails.length > MAX_APKS_TO_QUERY) { + throw new IllegalArgumentException( + "Cannot query more than " + MAX_APKS_TO_QUERY + ". " + + "You tried to query " + apkDetails.length); + } for (int i = 0; i < apkDetails.length; i ++) { String[] parts = apkDetails[i].split(":"); String id = parts[0]; diff --git a/test/src/org/fdroid/fdroid/ApkProviderTest.java b/test/src/org/fdroid/fdroid/ApkProviderTest.java new file mode 100644 index 000000000..795172d27 --- /dev/null +++ b/test/src/org/fdroid/fdroid/ApkProviderTest.java @@ -0,0 +1,131 @@ +package org.fdroid.fdroid; + +import android.content.ContentValues; +import android.database.Cursor; +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.ApkProvider; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.mock.MockApk; + +import java.util.ArrayList; +import java.util.List; + +public class ApkProviderTest extends FDroidProviderTest { + + public ApkProviderTest() { + super(ApkProvider.class, ApkProvider.getAuthority()); + } + + protected String[] getMinimalProjection() { + return new String[] { + ApkProvider.DataColumns.APK_ID, + ApkProvider.DataColumns.VERSION_CODE, + ApkProvider.DataColumns.NAME + }; + } + + public void testUris() { + assertInvalidUri(ApkProvider.getAuthority()); + assertInvalidUri(AppProvider.getContentUri()); + + List apks = new ArrayList(3); + for (int i = 0; i < 10; i ++) { + apks.add(new MockApk("com.example." + i, i)); + } + + assertValidUri(ApkProvider.getContentUri()); + assertValidUri(ApkProvider.getAppUri("org.fdroid.fdroid")); + assertValidUri(ApkProvider.getContentUri(new MockApk("org.fdroid.fdroid", 100))); + assertValidUri(ApkProvider.getContentUri()); + assertValidUri(ApkProvider.getContentUri(apks)); + assertValidUri(ApkProvider.getContentUri("org.fdroid.fdroid", 100)); + assertValidUri(ApkProvider.getRepoUri(1000)); + + List manyApks = new ArrayList(ApkProvider.MAX_APKS_TO_QUERY - 5); + for (int i = 0; i < ApkProvider.MAX_APKS_TO_QUERY - 1; i ++) { + manyApks.add(new MockApk("com.example." + i, i)); + } + assertValidUri(ApkProvider.getContentUri(manyApks)); + + manyApks.add(new MockApk("org.fdroid.fdroid.1", 1)); + manyApks.add(new MockApk("org.fdroid.fdroid.2", 2)); + try { + // Technically, it is a valid URI, because it doesn't + // throw an UnsupportedOperationException. However it + // is still not okay (we run out of bindable parameters + // in the sqlite query. + assertValidUri(ApkProvider.getContentUri(manyApks)); + fail(); + } catch (IllegalArgumentException e) { + // This is the expected error behaviour. + } catch (Exception e) { + fail(); + } + + } + + public void testQuery() { + Cursor cursor = queryAllApks(); + assertNotNull(cursor); + } + + private void insertApks(int count) { + for (int i = 0; i < count; i ++) { + insertApk("com.example.test." + i, i); + } + } + + public void testInsert() { + + // Start with an empty database... + Cursor cursor = queryAllApks(); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + + // Insert a new record... + insertApk("org.fdroid.fdroid", 13); + cursor = queryAllApks(); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + + // We intentionally throw an IllegalArgumentException if you haven't + // yet called cursor.move*()... + try { + new Apk(cursor); + fail(); + } catch (IllegalArgumentException e) { + // Success! + } catch (Exception e) { + fail(); + } + + // And now we should be able to recover these values from the apk + // value object (because the queryAllApks() helper asks for VERSION_CODE and + // APK_ID. + cursor.moveToFirst(); + Apk apk = new Apk(cursor); + assertEquals("org.fdroid.fdroid", apk.id); + assertEquals(13, apk.vercode); + } + + public void testIgnore() { + for (int i = 0; i < 10; i ++) { + insertApk("org.fdroid.fdroid", i); + } + + } + + private Cursor queryAllApks() { + return getMockContentResolver().query(ApkProvider.getContentUri(), getMinimalProjection(), null, null, null); + } + + private void insertApk(String id, int versionCode) { + insertApk(id, versionCode, new ContentValues()); + } + + private void insertApk(String id, int versionCode, + ContentValues additionalValues) { + TestUtils.insertApk(getMockContentResolver(), id, versionCode, additionalValues); + } + +} diff --git a/test/src/org/fdroid/fdroid/AppProviderTest.java b/test/src/org/fdroid/fdroid/AppProviderTest.java index 3252fd829..ae10ffc34 100644 --- a/test/src/org/fdroid/fdroid/AppProviderTest.java +++ b/test/src/org/fdroid/fdroid/AppProviderTest.java @@ -3,7 +3,6 @@ package org.fdroid.fdroid; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; -import junit.framework.AssertionFailedError; import mock.MockCategoryResources; import mock.MockInstallablePackageManager; import org.fdroid.fdroid.data.ApkProvider; @@ -11,7 +10,6 @@ import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppProvider; import java.util.ArrayList; -import java.util.Collections; import java.util.List; public class AppProviderTest extends FDroidProviderTest { @@ -24,7 +22,6 @@ public class AppProviderTest extends FDroidProviderTest { public void setUp() throws Exception { super.setUp(); getSwappableContext().setResources(new MockCategoryResources()); - getSwappableContext().setContentResolver(getMockContentResolver()); } protected String[] getMinimalProjection() { @@ -75,20 +72,14 @@ public class AppProviderTest extends FDroidProviderTest { insertApps(100); - assertAppCount(100, AppProvider.getContentUri()); - assertAppCount(0, AppProvider.getInstalledUri()); + assertResultCount(100, AppProvider.getContentUri()); + assertResultCount(0, AppProvider.getInstalledUri()); for (int i = 10; i < 20; i ++) { pm.install("com.example.test." + i, i, "v1"); } - assertAppCount(10, AppProvider.getInstalledUri()); - } - - private void assertAppCount(int expectedCount, Uri uri) { - Cursor cursor = getMockContentResolver().query(uri, getMinimalProjection(), null, null, null); - assertNotNull(cursor); - assertEquals(expectedCount, cursor.getCount()); + assertResultCount(10, AppProvider.getInstalledUri()); } public void testInsert() { @@ -142,7 +133,7 @@ public class AppProviderTest extends FDroidProviderTest { "Mineral", "Vegetable" }; - assertContainsOnly(categories, expected); + TestUtils.assertContainsOnly(categories, expected); } public void testCategoriesMultiple() { @@ -160,7 +151,7 @@ public class AppProviderTest extends FDroidProviderTest { "Mineral", "Vegetable" }; - assertContainsOnly(categories, expected); + TestUtils.assertContainsOnly(categories, expected); insertAppWithCategory("com.example.game", "Game", "Running,Shooting,Jumping,Bleh,Sneh,Pleh,Blah,Test category," + @@ -188,7 +179,7 @@ public class AppProviderTest extends FDroidProviderTest { "With apostrophe's" }; - assertContainsOnly(categoriesLonger, expectedLonger); + TestUtils.assertContainsOnly(categoriesLonger, expectedLonger); } private void insertApp(String id, String name) { @@ -207,52 +198,4 @@ public class AppProviderTest extends FDroidProviderTest { TestUtils.insertApp(getMockContentResolver(), id, name, additionalValues); } - private void assertContainsOnly(List actualList, T[] expectedContains) { - List containsList = new ArrayList(expectedContains.length); - Collections.addAll(containsList, expectedContains); - assertContainsOnly(actualList, containsList); - } - - private String listToString(List list) { - String string = "["; - for (int i = 0; i < list.size(); i ++) { - if (i > 0) { - string += ", "; - } - string += list.get(i); - } - string += "]"; - return string; - } - - private void assertContainsOnly(List actualList, List expectedContains) { - if (actualList.size() != expectedContains.size()) { - String message = - "List sizes don't match.\n" + - "Expected: " + - listToString(expectedContains) + "\n" + - "Actual: " + - listToString(actualList); - throw new AssertionFailedError(message); - } - for (T required : expectedContains) { - boolean containsRequired = false; - for (T itemInList : actualList) { - if (required.equals(itemInList)) { - containsRequired = true; - break; - } - } - if (!containsRequired) { - String message = - "List doesn't contain \"" + required + "\".\n" + - "Expected: " + - listToString(expectedContains) + "\n" + - "Actual: " + - listToString(actualList); - throw new AssertionFailedError(message); - } - } - } - } diff --git a/test/src/org/fdroid/fdroid/FDroidProviderTest.java b/test/src/org/fdroid/fdroid/FDroidProviderTest.java index 776202ba0..fe85ea6d2 100644 --- a/test/src/org/fdroid/fdroid/FDroidProviderTest.java +++ b/test/src/org/fdroid/fdroid/FDroidProviderTest.java @@ -24,6 +24,13 @@ public abstract class FDroidProviderTest extends Provi public void setUp() throws Exception { super.setUp(); Utils.setupInstalledApkCache(new MockInstalledApkCache()); + + // The *Provider.Helper.* functions tend to take a Context as their + // first parameter. This context is used to connect to the relevant + // content provider. Thus, we need a context that is able to connect + // to the mock content resolver, in order to reach the provider + // under test. + getSwappableContext().setContentResolver(getMockContentResolver()); } @TargetApi(Build.VERSION_CODES.ECLAIR) @@ -74,4 +81,9 @@ public abstract class FDroidProviderTest extends Provi */ protected abstract String[] getMinimalProjection(); + protected void assertResultCount(int expectedCount, Uri uri) { + Cursor cursor = getMockContentResolver().query(uri, getMinimalProjection(), null, null, null); + assertNotNull(cursor); + assertEquals(expectedCount, cursor.getCount()); + } } diff --git a/test/src/org/fdroid/fdroid/TestUtils.java b/test/src/org/fdroid/fdroid/TestUtils.java index a01f85460..19299e10a 100644 --- a/test/src/org/fdroid/fdroid/TestUtils.java +++ b/test/src/org/fdroid/fdroid/TestUtils.java @@ -2,10 +2,65 @@ package org.fdroid.fdroid; import android.content.*; import android.net.Uri; +import android.test.mock.MockContentResolver; +import junit.framework.AssertionFailedError; +import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.AppProvider; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + public class TestUtils { + public static void assertContainsOnly(List actualList, T[] expectedContains) { + List containsList = new ArrayList(expectedContains.length); + Collections.addAll(containsList, expectedContains); + assertContainsOnly(actualList, containsList); + } + + public static String listToString(List list) { + String string = "["; + for (int i = 0; i < list.size(); i ++) { + if (i > 0) { + string += ", "; + } + string += list.get(i); + } + string += "]"; + return string; + } + + public static void assertContainsOnly(List actualList, List expectedContains) { + if (actualList.size() != expectedContains.size()) { + String message = + "List sizes don't match.\n" + + "Expected: " + + listToString(expectedContains) + "\n" + + "Actual: " + + listToString(actualList); + throw new AssertionFailedError(message); + } + for (T required : expectedContains) { + boolean containsRequired = false; + for (T itemInList : actualList) { + if (required.equals(itemInList)) { + containsRequired = true; + break; + } + } + if (!containsRequired) { + String message = + "List doesn't contain \"" + required + "\".\n" + + "Expected: " + + listToString(expectedContains) + "\n" + + "Actual: " + + listToString(actualList); + throw new AssertionFailedError(message); + } + } + } + public static void insertApp(ContentResolver resolver, String id, String name, ContentValues additionalValues) { ContentValues values = new ContentValues(); @@ -27,4 +82,25 @@ public class TestUtils { resolver.insert(uri, values); } + public static void insertApk(ContentResolver resolver, String id, int versionCode, ContentValues additionalValues) { + + ContentValues values = new ContentValues(); + + values.put(ApkProvider.DataColumns.APK_ID, id); + values.put(ApkProvider.DataColumns.VERSION_CODE, versionCode); + + // Required fields (NOT NULL in the database). + values.put(ApkProvider.DataColumns.REPO_ID, 1); + values.put(ApkProvider.DataColumns.VERSION, "The good one"); + values.put(ApkProvider.DataColumns.HASH, "11111111aaaaaaaa"); + values.put(ApkProvider.DataColumns.NAME, "Test Apk"); + values.put(ApkProvider.DataColumns.SIZE, 10000); + values.put(ApkProvider.DataColumns.IS_COMPATIBLE, 1); + + values.putAll(additionalValues); + + Uri uri = ApkProvider.getContentUri(); + + resolver.insert(uri, values); + } } diff --git a/test/src/org/fdroid/fdroid/mock/MockApk.java b/test/src/org/fdroid/fdroid/mock/MockApk.java new file mode 100644 index 000000000..f3da31d6d --- /dev/null +++ b/test/src/org/fdroid/fdroid/mock/MockApk.java @@ -0,0 +1,12 @@ +package org.fdroid.fdroid.mock; + +import org.fdroid.fdroid.data.Apk; + +public class MockApk extends Apk { + + public MockApk(String id, int versionCode) { + this.id = id; + this.vercode = versionCode; + } + +} diff --git a/test/src/org/fdroid/fdroid/mock/MockApp.java b/test/src/org/fdroid/fdroid/mock/MockApp.java new file mode 100644 index 000000000..1a983865c --- /dev/null +++ b/test/src/org/fdroid/fdroid/mock/MockApp.java @@ -0,0 +1,16 @@ +package org.fdroid.fdroid.mock; + +import org.fdroid.fdroid.data.App; + +public class MockApp extends App { + + public MockApp(String id) { + this(id, "App " + id); + } + + public MockApp(String id, String name) { + this.id = id; + this.name = name; + } + +} From 68a719f48a9b06e8d54891ed7b4e68f5b6090f34 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 18 Feb 2014 08:11:02 +1100 Subject: [PATCH 118/282] Don't overwrite "ignore updates" settings on update. For now, the UpdateService ignores these fields when updating from the index. There is no time that the index should specify what versions to be ignored. In the future, this will be done with a join table that stores info about what to ignore. Another future improvement should also be to make "App.toContentValues()" smarter. That is, make it only return values which have been set since the object was created. However this will add an overhead which may or may not be noticable. --- TODO-before-release.md | 3 --- src/org/fdroid/fdroid/UpdateService.java | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/TODO-before-release.md b/TODO-before-release.md index b755ba856..1a0d72a39 100644 --- a/TODO-before-release.md +++ b/TODO-before-release.md @@ -1,8 +1,5 @@ These issues are a must-fix before the next stable release: -* Move Ignore settings into separate table to not overwrite them upon repo - update - * Right after updating a repo, "Recently Updated" shows the apps correctly but the new apks don't show up on App Details until the whole app is restarted (or until the repos are wiped and re-downloaded) diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index ac7689b9a..915da5e82 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -55,6 +55,20 @@ public class UpdateService extends IntentService implements ProgressListener { super("UpdateService"); } + /** + * When an app already exists in the db, and we are updating it on the off chance that some + * values changed in the index, some fields should not be updated. Rather, they should be + * ignored, because they were explicitly set by the user, and hence can't be automatically + * overridden by the index. + * + * NOTE: In the future, these attributes will be moved to a join table, so that the app table + * is essentially completely transient, and can be nuked at any time. + */ + private static final String[] APP_FIELDS_TO_IGNORE = { + AppProvider.DataColumns.IGNORE_ALLUPDATES, + AppProvider.DataColumns.IGNORE_THISUPDATE + }; + // 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 { @@ -644,6 +658,11 @@ public class UpdateService extends IntentService implements ProgressListener { private ContentProviderOperation updateExistingApp(App app) { Uri uri = AppProvider.getContentUri(app); ContentValues values = app.toContentValues(); + for (String toIgnore : APP_FIELDS_TO_IGNORE) { + if (values.containsKey(toIgnore)) { + values.remove(toIgnore); + } + } return ContentProviderOperation.newUpdate(uri).withValues(values).build(); } From 4b4ee4b6db324116dbc7b36a39173b9f7056874c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 18 Feb 2014 11:26:15 +0100 Subject: [PATCH 119/282] Update MTM module, now supports gradle --- extern/MemorizingTrustManager | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/MemorizingTrustManager b/extern/MemorizingTrustManager index 49452f67a..a705441ac 160000 --- a/extern/MemorizingTrustManager +++ b/extern/MemorizingTrustManager @@ -1 +1 @@ -Subproject commit 49452f67a760dfef77ddaa7e0b7d88c713c4a195 +Subproject commit a705441ac53b9e1aba9f00f3f59aab81da6fbc9e From 4860e06af3159f0412ab415eca0867c71e602cb2 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 19 Feb 2014 08:12:21 +1100 Subject: [PATCH 120/282] Improved apk tests (test deleting). --- src/org/fdroid/fdroid/data/ApkProvider.java | 45 +++-- .../fdroid/fdroid/data/FDroidProvider.java | 20 +++ src/org/fdroid/fdroid/data/Repo.java | 2 +- .../org/fdroid/fdroid/ApkProviderTest.java | 167 ++++++++++++++++-- .../org/fdroid/fdroid/FDroidProviderTest.java | 18 +- test/src/org/fdroid/fdroid/TestUtils.java | 4 +- test/src/org/fdroid/fdroid/mock/MockRepo.java | 11 ++ 7 files changed, 227 insertions(+), 40 deletions(-) create mode 100644 test/src/org/fdroid/fdroid/mock/MockRepo.java diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java index b957b2093..6c9afbdeb 100644 --- a/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -64,7 +64,7 @@ public class ApkProvider extends FDroidProvider { return cursorToList(cursor); } - private static List cursorToList(Cursor cursor) { + public static List cursorToList(Cursor cursor) { List apks = new ArrayList(); if (cursor != null) { cursor.moveToFirst(); @@ -77,20 +77,16 @@ public class ApkProvider extends FDroidProvider { return apks; } - public static void deleteApksByRepo(Context context, Repo repo) { + public static int deleteApksByRepo(Context context, Repo repo) { ContentResolver resolver = context.getContentResolver(); - Uri uri = getContentUri(); - String[] args = { Long.toString(repo.getId()) }; - String selection = DataColumns.REPO_ID + " = ?"; - int count = resolver.delete(uri, selection, args); + Uri uri = getRepoUri(repo.getId()); + return resolver.delete(uri, null, null); } public static void deleteApksByApp(Context context, App app) { ContentResolver resolver = context.getContentResolver(); - Uri uri = getContentUri(); - String[] args = { app.id }; - String selection = DataColumns.APK_ID + " = ?"; - resolver.delete(uri, selection, args); + Uri uri = getAppUri(app.id); + resolver.delete(uri, null, null); } public static Apk find(Context context, String id, int versionCode) { @@ -340,7 +336,7 @@ public class ApkProvider extends FDroidProvider { } private QuerySelection queryApp(String appId) { - String selection = " id = ? "; + String selection = DataColumns.APK_ID + " = ? "; String[] args = new String[] { appId }; return new QuerySelection(selection, args); } @@ -357,7 +353,7 @@ public class ApkProvider extends FDroidProvider { } private QuerySelection queryRepo(long repoId) { - String selection = " repo = ? "; + String selection = DataColumns.REPO_ID + " = ? "; String[] args = new String[]{ Long.toString(repoId) }; return new QuerySelection(selection, args); } @@ -442,6 +438,7 @@ public class ApkProvider extends FDroidProvider { @Override public Uri insert(Uri uri, ContentValues values) { removeRepoFields(values); + validateFields(DataColumns.ALL, values); long id = write().insertOrThrow(getTableName(), null, values); if (!isApplyingBatch()) { getContext().getContentResolver().notifyChange(uri, null); @@ -458,18 +455,30 @@ public class ApkProvider extends FDroidProvider { QuerySelection query = new QuerySelection(where, whereArgs); switch (matcher.match(uri)) { - case CODE_LIST: - // Don't support deleting of multiple apks yet. - return 0; case CODE_REPO: query = query.add(queryRepo(Long.parseLong(uri.getLastPathSegment()))); break; - case CODE_SINGLE: - query = query.add(querySingle(uri)); + case CODE_APP: + query = query.add(queryApp(uri.getLastPathSegment())); break; + case CODE_LIST: + throw new UnsupportedOperationException( + "Can't delete all apks. " + + "Can only delete those belonging to an app, or a repo."); + + case CODE_APKS: + throw new UnsupportedOperationException( + "Can't delete arbitrary apks. " + + "Can only delete those belonging to an app, or a repo."); + + case CODE_SINGLE: + throw new UnsupportedOperationException( + "Can't delete individual apks. " + + "Can only delete those belonging to an app, or a repo."); + default: Log.e("FDroid", "Invalid URI for apk content provider: " + uri); throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri); @@ -486,6 +495,8 @@ public class ApkProvider extends FDroidProvider { QuerySelection query = new QuerySelection(where, whereArgs); + validateFields(DataColumns.ALL, values); + switch (matcher.match(uri)) { case CODE_LIST: return 0; diff --git a/src/org/fdroid/fdroid/data/FDroidProvider.java b/src/org/fdroid/fdroid/data/FDroidProvider.java index 3e0c4b316..e056eef3e 100644 --- a/src/org/fdroid/fdroid/data/FDroidProvider.java +++ b/src/org/fdroid/fdroid/data/FDroidProvider.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid.data; +import android.annotation.TargetApi; import android.content.*; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; @@ -95,5 +96,24 @@ public abstract class FDroidProvider extends ContentProvider { } return sb.toString(); } + + @TargetApi(11) + protected void validateFields(String[] validFields, ContentValues values) + throws IllegalArgumentException { + for (String key : values.keySet()) { + boolean isValid = false; + for (String validKey : validFields) { + if (validKey.equals(key)) { + isValid = true; + break; + } + } + + if (!isValid) { + throw new IllegalArgumentException( + "Cannot save field '" + key + "' to provider " + getProviderName()); + } + } + } } diff --git a/src/org/fdroid/fdroid/data/Repo.java b/src/org/fdroid/fdroid/data/Repo.java index d5bc3bc56..1b0bdd055 100644 --- a/src/org/fdroid/fdroid/data/Repo.java +++ b/src/org/fdroid/fdroid/data/Repo.java @@ -14,7 +14,7 @@ public class Repo extends ValueObject { public static final int VERSION_DENSITY_SPECIFIC_ICONS = 11; - private long id; + protected long id; public String address; public String name; diff --git a/test/src/org/fdroid/fdroid/ApkProviderTest.java b/test/src/org/fdroid/fdroid/ApkProviderTest.java index 795172d27..0785a87db 100644 --- a/test/src/org/fdroid/fdroid/ApkProviderTest.java +++ b/test/src/org/fdroid/fdroid/ApkProviderTest.java @@ -2,10 +2,14 @@ package org.fdroid.fdroid; import android.content.ContentValues; import android.database.Cursor; +import android.net.Uri; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.mock.MockApk; +import org.fdroid.fdroid.mock.MockApp; +import org.fdroid.fdroid.mock.MockRepo; import java.util.ArrayList; import java.util.List; @@ -20,13 +24,14 @@ public class ApkProviderTest extends FDroidProviderTest { return new String[] { ApkProvider.DataColumns.APK_ID, ApkProvider.DataColumns.VERSION_CODE, - ApkProvider.DataColumns.NAME + ApkProvider.DataColumns.NAME, + ApkProvider.DataColumns.REPO_ID }; } public void testUris() { assertInvalidUri(ApkProvider.getAuthority()); - assertInvalidUri(AppProvider.getContentUri()); + assertInvalidUri(RepoProvider.getContentUri()); List apks = new ArrayList(3); for (int i = 0; i < 10; i ++) { @@ -61,7 +66,86 @@ public class ApkProviderTest extends FDroidProviderTest { } catch (Exception e) { fail(); } + } + public void testAppApks() { + for (int i = 1; i <= 10; i ++) { + insertApk("org.fdroid.fdroid", i); + insertApk("com.example", i); + } + + assertTotalApkCount(20); + + Cursor fdroidApks = getMockContentResolver().query( + ApkProvider.getAppUri("org.fdroid.fdroid"), + getMinimalProjection(), + null, null, null); + assertResultCount(10, fdroidApks); + assertBelongsToApp(fdroidApks, "org.fdroid.fdroid"); + + Cursor exampleApks = getMockContentResolver().query( + ApkProvider.getAppUri("com.example"), + getMinimalProjection(), + null, null, null); + assertResultCount(10, exampleApks); + assertBelongsToApp(exampleApks, "com.example"); + + ApkProvider.Helper.deleteApksByApp(getMockContext(), new MockApp("com.example")); + + Cursor all = queryAllApks(); + assertResultCount(10, all); + assertBelongsToApp(all, "org.fdroid.fdroid"); + } + + public void testInvalidDeleteUris() { + assertCantDelete(ApkProvider.getContentUri()); + assertCantDelete(ApkProvider.getContentUri(new ArrayList())); + assertCantDelete(ApkProvider.getContentUri("org.fdroid.fdroid", 10)); + assertCantDelete(ApkProvider.getContentUri(new MockApk("org.fdroid.fdroid", 10))); + + try { + getMockContentResolver().delete(RepoProvider.getContentUri(), null, null); + fail(); + } catch (IllegalArgumentException e) { + // Don't fail, it is what we were looking for... + } catch (Exception e) { + fail(); + } + } + + public void testRepoApks() { + + final long REPO_KEEP = 1; + final long REPO_DELETE = 2; + + // Insert apks into two repos, one of which we will later purge the + // the apks from. + for (int i = 1; i <= 5; i ++) { + insertApkForRepo("org.fdroid.fdroid", i, REPO_KEEP); + insertApkForRepo("com.example." + i, 1, REPO_DELETE); + } + for (int i = 6; i <= 10; i ++) { + insertApkForRepo("org.fdroid.fdroid", i, REPO_DELETE); + insertApkForRepo("com.example." + i, 1, REPO_KEEP); + } + + assertTotalApkCount(20); + + Cursor cursor = getMockContentResolver().query( + ApkProvider.getRepoUri(REPO_DELETE), getMinimalProjection(), null, null, null); + assertResultCount(10, cursor); + assertBelongsToRepo(cursor, REPO_DELETE); + + int count = ApkProvider.Helper.deleteApksByRepo(getMockContext(), new MockRepo(REPO_DELETE)); + assertEquals(10, count); + + assertTotalApkCount(10); + cursor = getMockContentResolver().query( + ApkProvider.getRepoUri(REPO_DELETE), getMinimalProjection(), null, null, null); + assertResultCount(0, cursor); + + // The only remaining apks should be those from REPO_KEEP. + assertBelongsToRepo(queryAllApks(), REPO_KEEP); } public void testQuery() { @@ -69,12 +153,6 @@ public class ApkProviderTest extends FDroidProviderTest { assertNotNull(cursor); } - private void insertApks(int count) { - for (int i = 0; i < count; i ++) { - insertApk("com.example.test." + i, i); - } - } - public void testInsert() { // Start with an empty database... @@ -82,8 +160,11 @@ public class ApkProviderTest extends FDroidProviderTest { assertNotNull(cursor); assertEquals(0, cursor.getCount()); + Apk apk = new MockApk("org.fdroid.fdroid", 13); + // Insert a new record... - insertApk("org.fdroid.fdroid", 13); + Uri newUri = insertApk(apk.id, apk.vercode); + assertEquals(ApkProvider.getContentUri(apk).toString(), newUri.toString()); cursor = queryAllApks(); assertNotNull(cursor); assertEquals(1, cursor.getCount()); @@ -103,29 +184,79 @@ public class ApkProviderTest extends FDroidProviderTest { // value object (because the queryAllApks() helper asks for VERSION_CODE and // APK_ID. cursor.moveToFirst(); - Apk apk = new Apk(cursor); - assertEquals("org.fdroid.fdroid", apk.id); - assertEquals(13, apk.vercode); + Apk toCheck = new Apk(cursor); + assertEquals("org.fdroid.fdroid", toCheck.id); + assertEquals(13, toCheck.vercode); + } + + public void testInsertWithExtraFields() { + + assertResultCount(0, queryAllApks()); + + String[] repoFields = new String[] { + RepoProvider.DataColumns.DESCRIPTION, + RepoProvider.DataColumns.ADDRESS, + RepoProvider.DataColumns.FINGERPRINT, + RepoProvider.DataColumns.NAME, + RepoProvider.DataColumns.PUBLIC_KEY + }; + + for (String field : repoFields) { + ContentValues invalidRepo = new ContentValues(); + invalidRepo.put(field, "Test data"); + try { + insertApk("org.fdroid.fdroid", 10, invalidRepo); + fail(); + } catch (IllegalArgumentException e) { + } catch (Exception e) { + fail(); + } + assertResultCount(0, queryAllApks()); + } + + // ApkProvider.DataColumns.REPO + } public void testIgnore() { - for (int i = 0; i < 10; i ++) { + /*for (int i = 0; i < 10; i ++) { insertApk("org.fdroid.fdroid", i); - } + }*/ + } + private void assertBelongsToApp(Cursor apks, String appId) { + for (Apk apk : ApkProvider.Helper.cursorToList(apks)) { + assertEquals(appId, apk.id); + } + } + + private void assertTotalApkCount(int expected) { + assertResultCount(expected, queryAllApks()); + } + + private void assertBelongsToRepo(Cursor apkCursor, long repoId) { + for (Apk apk : ApkProvider.Helper.cursorToList(apkCursor)) { + assertEquals(repoId, apk.repo); + } + } + + private void insertApkForRepo(String id, int versionCode, long repoId) { + ContentValues additionalValues = new ContentValues(); + additionalValues.put(ApkProvider.DataColumns.REPO_ID, repoId); + insertApk(id, versionCode, additionalValues); } private Cursor queryAllApks() { return getMockContentResolver().query(ApkProvider.getContentUri(), getMinimalProjection(), null, null, null); } - private void insertApk(String id, int versionCode) { - insertApk(id, versionCode, new ContentValues()); + private Uri insertApk(String id, int versionCode) { + return insertApk(id, versionCode, new ContentValues()); } - private void insertApk(String id, int versionCode, + private Uri insertApk(String id, int versionCode, ContentValues additionalValues) { - TestUtils.insertApk(getMockContentResolver(), id, versionCode, additionalValues); + return TestUtils.insertApk(getMockContentResolver(), id, versionCode, additionalValues); } } diff --git a/test/src/org/fdroid/fdroid/FDroidProviderTest.java b/test/src/org/fdroid/fdroid/FDroidProviderTest.java index fe85ea6d2..1fe2e3c48 100644 --- a/test/src/org/fdroid/fdroid/FDroidProviderTest.java +++ b/test/src/org/fdroid/fdroid/FDroidProviderTest.java @@ -50,6 +50,16 @@ public abstract class FDroidProviderTest extends Provi return swappableContext; } + protected void assertCantDelete(Uri uri) { + try { + getMockContentResolver().delete(uri, null, null); + fail(); + } catch (UnsupportedOperationException e) { + } catch (Exception e) { + fail(); + } + } + protected void assertInvalidUri(String uri) { assertInvalidUri(Uri.parse(uri)); } @@ -83,7 +93,11 @@ public abstract class FDroidProviderTest extends Provi protected void assertResultCount(int expectedCount, Uri uri) { Cursor cursor = getMockContentResolver().query(uri, getMinimalProjection(), null, null, null); - assertNotNull(cursor); - assertEquals(expectedCount, cursor.getCount()); + assertResultCount(expectedCount, cursor); + } + + protected void assertResultCount(int expectedCount, Cursor result) { + assertNotNull(result); + assertEquals(expectedCount, result.getCount()); } } diff --git a/test/src/org/fdroid/fdroid/TestUtils.java b/test/src/org/fdroid/fdroid/TestUtils.java index 19299e10a..420c32c45 100644 --- a/test/src/org/fdroid/fdroid/TestUtils.java +++ b/test/src/org/fdroid/fdroid/TestUtils.java @@ -82,7 +82,7 @@ public class TestUtils { resolver.insert(uri, values); } - public static void insertApk(ContentResolver resolver, String id, int versionCode, ContentValues additionalValues) { + public static Uri insertApk(ContentResolver resolver, String id, int versionCode, ContentValues additionalValues) { ContentValues values = new ContentValues(); @@ -101,6 +101,6 @@ public class TestUtils { Uri uri = ApkProvider.getContentUri(); - resolver.insert(uri, values); + return resolver.insert(uri, values); } } diff --git a/test/src/org/fdroid/fdroid/mock/MockRepo.java b/test/src/org/fdroid/fdroid/mock/MockRepo.java new file mode 100644 index 000000000..3b3fce976 --- /dev/null +++ b/test/src/org/fdroid/fdroid/mock/MockRepo.java @@ -0,0 +1,11 @@ +package org.fdroid.fdroid.mock; + +import org.fdroid.fdroid.data.Repo; + +public class MockRepo extends Repo { + + public MockRepo(long repoId) { + id = repoId; + } + +} From a797e43178022d5bc33487a1b028f2301496f3b4 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 19 Feb 2014 09:32:54 +1100 Subject: [PATCH 121/282] Test apk insert more comprehensivly. --- .../org/fdroid/fdroid/ApkProviderTest.java | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/test/src/org/fdroid/fdroid/ApkProviderTest.java b/test/src/org/fdroid/fdroid/ApkProviderTest.java index 0785a87db..4c808a066 100644 --- a/test/src/org/fdroid/fdroid/ApkProviderTest.java +++ b/test/src/org/fdroid/fdroid/ApkProviderTest.java @@ -214,8 +214,37 @@ public class ApkProviderTest extends FDroidProviderTest { assertResultCount(0, queryAllApks()); } - // ApkProvider.DataColumns.REPO + ContentValues values = new ContentValues(); + values.put(ApkProvider.DataColumns.REPO_ID, 10); + values.put(ApkProvider.DataColumns.REPO_ADDRESS, "http://example.com"); + values.put(ApkProvider.DataColumns.REPO_VERSION, 3); + values.put(ApkProvider.DataColumns.FEATURES, "Some features"); + Uri uri = insertApk("com.example.com", 1, values); + assertResultCount(1, queryAllApks()); + + String[] projections = { + ApkProvider.DataColumns.REPO_ID, + ApkProvider.DataColumns.REPO_ADDRESS, + ApkProvider.DataColumns.REPO_VERSION, + ApkProvider.DataColumns.FEATURES, + ApkProvider.DataColumns.APK_ID, + ApkProvider.DataColumns.VERSION_CODE + }; + + Cursor cursor = getMockContentResolver().query(uri, projections, null, null, null); + cursor.moveToFirst(); + Apk apk = new Apk(cursor); + + // These should have quietly been dropped when we tried to save them... + assertEquals(null, apk.repoAddress); + assertEquals(0, apk.repoVersion); + + // But this should have saved correctly... + assertEquals("Some features", apk.features.toString()); + assertEquals("com.example.com", apk.id); + assertEquals(1, apk.vercode); + assertEquals(10, apk.repo); } public void testIgnore() { From 226003d38d9f6990f70fd93b3475306fd40caad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 19 Feb 2014 00:19:05 +0100 Subject: [PATCH 122/282] Git automatic indenting fix --- test/src/org/fdroid/fdroid/FDroidProviderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/org/fdroid/fdroid/FDroidProviderTest.java b/test/src/org/fdroid/fdroid/FDroidProviderTest.java index 30f4f52e9..b7497463b 100644 --- a/test/src/org/fdroid/fdroid/FDroidProviderTest.java +++ b/test/src/org/fdroid/fdroid/FDroidProviderTest.java @@ -30,7 +30,7 @@ public abstract class FDroidProviderTest extends Provi // content provider. Thus, we need a context that is able to connect // to the mock content resolver, in order to reach the provider // under test. - getSwappableContext().setContentResolver(getMockContentResolver()); + getSwappableContext().setContentResolver(getMockContentResolver()); } @TargetApi(Build.VERSION_CODES.ECLAIR) From 334ba0b956d0dd47fcc860a540a5e73b1eea2aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 19 Feb 2014 00:22:46 +0100 Subject: [PATCH 123/282] Remove TODO bluetooth item After discussing it with Hans, the two points that I stated here are not correct. --- TODO-before-release.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/TODO-before-release.md b/TODO-before-release.md index 1a0d72a39..ced948976 100644 --- a/TODO-before-release.md +++ b/TODO-before-release.md @@ -3,9 +3,3 @@ These issues are a must-fix before the next stable release: * Right after updating a repo, "Recently Updated" shows the apps correctly but the new apks don't show up on App Details until the whole app is restarted (or until the repos are wiped and re-downloaded) - -Other minor issues: - -* Make the bluetooth option prettier. Options: - - Move it into submenu (like "Share F-Droid" -> "Bluetooth/Mail/NFC/...") - - Remove ellipsis from menu option string From 09f3e8b004ba049a22bcaf5710f1abd4c382b485 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 18 Feb 2014 15:16:26 -0500 Subject: [PATCH 124/282] forgot to include the .project Eclipse file for the test project Include the reusable Eclipse files (.project and .classpath). --- test/.project | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/.project diff --git a/test/.project b/test/.project new file mode 100644 index 000000000..0c2b67ff9 --- /dev/null +++ b/test/.project @@ -0,0 +1,33 @@ + + + fdroid-test + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + From a8bf9f7614fbcfda6d43f7dc36b12a34b8264688 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 18 Feb 2014 16:12:46 -0500 Subject: [PATCH 125/282] point Eclipse to new location for UniversalImageLoader submodule --- .classpath | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.classpath b/.classpath index 37a11b83e..d2ce7e889 100644 --- a/.classpath +++ b/.classpath @@ -7,6 +7,6 @@ - + From ab6166c36d7fd3e2fbf1e32ed18210a27c5b0aac Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 18 Feb 2014 16:33:36 -0500 Subject: [PATCH 126/282] make RepoProvider.Helper take Context rather than ContentResolver This makes the code a bit neater, and passing the Context around is a common pattern. https://dev.guardianproject.info/issues/2926 refs #2926 --- src/org/fdroid/fdroid/AppDetails.java | 3 +- src/org/fdroid/fdroid/UpdateService.java | 2 +- src/org/fdroid/fdroid/data/RepoProvider.java | 44 +++++++++++-------- .../fdroid/fdroid/updater/RepoUpdater.java | 2 +- .../fdroid/views/RepoDetailsActivity.java | 2 +- .../views/fragments/RepoDetailsFragment.java | 11 +++-- .../views/fragments/RepoListFragment.java | 9 ++-- 7 files changed, 38 insertions(+), 35 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 7e2a09053..330201416 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -837,8 +837,7 @@ public class AppDetails extends ListActivity { // Install the version of this app denoted by 'app.curApk'. private void install(final Apk apk) { String [] projection = { RepoProvider.DataColumns.ADDRESS }; - Repo repo = RepoProvider.Helper.findById( - getContentResolver(), apk.repo, projection); + Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection); if (repo == null || repo.address == null) { return; } diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index 915da5e82..6f71da16b 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -248,7 +248,7 @@ public class UpdateService extends IntentService implements ProgressListener { // Grab some preliminary information, then we can release the // database while we do all the downloading, etc... int updates = 0; - List repos = RepoProvider.Helper.all(getContentResolver()); + List repos = RepoProvider.Helper.all(this); // Process each repo... Map appsToUpdate = new HashMap(); diff --git a/src/org/fdroid/fdroid/data/RepoProvider.java b/src/org/fdroid/fdroid/data/RepoProvider.java index 8f9199f5c..9d23b440d 100644 --- a/src/org/fdroid/fdroid/data/RepoProvider.java +++ b/src/org/fdroid/fdroid/data/RepoProvider.java @@ -20,12 +20,13 @@ public class RepoProvider extends FDroidProvider { private Helper() {} - public static Repo findById(ContentResolver resolver, long repoId) { - return findById(resolver, repoId, DataColumns.ALL); + public static Repo findById(Context context, long repoId) { + return findById(context, repoId, DataColumns.ALL); } - public static Repo findById(ContentResolver resolver, long repoId, + public static Repo findById(Context context, long repoId, String[] projection) { + ContentResolver resolver = context.getContentResolver(); Uri uri = RepoProvider.getContentUri(repoId); Cursor cursor = resolver.query(uri, projection, null, null, null); Repo repo = null; @@ -36,32 +37,33 @@ public class RepoProvider extends FDroidProvider { return repo; } - public static Repo findByAddress(ContentResolver resolver, - String address) { - return findByAddress(resolver, address, DataColumns.ALL); + public static Repo findByAddress(Context context, String address) { + return findByAddress(context, address, DataColumns.ALL); } - public static Repo findByAddress(ContentResolver resolver, + public static Repo findByAddress(Context context, String address, String[] projection) { List repos = findBy( - resolver, DataColumns.ADDRESS, address, projection); + context, DataColumns.ADDRESS, address, projection); return repos.size() > 0 ? repos.get(0) : null; } - public static List all(ContentResolver resolver) { - return all(resolver, DataColumns.ALL); + public static List all(Context context) { + return all(context, DataColumns.ALL); } - public static List all(ContentResolver resolver, String[] projection) { + public static List all(Context context, String[] projection) { + ContentResolver resolver = context.getContentResolver(); Uri uri = RepoProvider.getContentUri(); Cursor cursor = resolver.query(uri, projection, null, null, null); return cursorToList(cursor); } - private static List findBy(ContentResolver resolver, + private static List findBy(Context context, String fieldName, String fieldValue, String[] projection) { + ContentResolver resolver = context.getContentResolver(); Uri uri = RepoProvider.getContentUri(); String[] args = { fieldValue }; Cursor cursor = resolver.query( @@ -82,8 +84,9 @@ public class RepoProvider extends FDroidProvider { return repos; } - public static void update(ContentResolver resolver, Repo repo, + public static void update(Context context, Repo repo, ContentValues values) { + ContentResolver resolver = context.getContentResolver(); // Change the name to the new address. Next time we update the repo // index file, it will populate the name field with the proper @@ -143,31 +146,34 @@ public class RepoProvider extends FDroidProvider { * resolver, but I thought I'd put it here in the interests of having * each of the CRUD methods available in the helper class. */ - public static void insert(ContentResolver resolver, + public static void insert(Context context, ContentValues values) { + ContentResolver resolver = context.getContentResolver(); Uri uri = RepoProvider.getContentUri(); resolver.insert(uri, values); } - public static void remove(ContentResolver resolver, long repoId) { + public static void remove(Context context, long repoId) { + ContentResolver resolver = context.getContentResolver(); Uri uri = RepoProvider.getContentUri(repoId); resolver.delete(uri, null, null); } public static void purgeApps(Context context, Repo repo, FDroidApp app) { Uri apkUri = ApkProvider.getRepoUri(repo.getId()); - int apkCount = context.getContentResolver().delete(apkUri, null, null); + ContentResolver resolver = context.getContentResolver(); + int apkCount = resolver.delete(apkUri, null, null); Log.d("FDroid", "Removed " + apkCount + " apks from repo " + repo.name); Uri appUri = AppProvider.getNoApksUri(); - int appCount = context.getContentResolver().delete(appUri, null, null); + int appCount = resolver.delete(appUri, null, null); Log.d("Log", "Removed " + appCount + " apps with no apks."); app.invalidateAllApps(); } - public static int countAppsForRepo(ContentResolver resolver, - long repoId) { + public static int countAppsForRepo(Context context, long repoId) { + ContentResolver resolver = context.getContentResolver(); String[] projection = { "COUNT(distinct id)" }; String selection = "repo = ?"; String[] args = { Long.toString(repoId) }; diff --git a/src/org/fdroid/fdroid/updater/RepoUpdater.java b/src/org/fdroid/fdroid/updater/RepoUpdater.java index b5c17fb07..2096a2db8 100644 --- a/src/org/fdroid/fdroid/updater/RepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/RepoUpdater.java @@ -263,7 +263,7 @@ abstract public class RepoUpdater { values.put(RepoProvider.DataColumns.NAME, handler.getName()); } - RepoProvider.Helper.update(context.getContentResolver(), repo, values); + RepoProvider.Helper.update(context, repo, values); } public static class UpdateException extends Exception { diff --git a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java index 5d664410b..533c08242 100644 --- a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -54,7 +54,7 @@ public class RepoDetailsActivity extends FragmentActivity { RepoProvider.DataColumns.ADDRESS, RepoProvider.DataColumns.FINGERPRINT }; - repo = RepoProvider.Helper.findById(getContentResolver(), repoId, projection); + repo = RepoProvider.Helper.findById(this, repoId, projection); ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); setTitle(repo.getName()); diff --git a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java index 4e762d238..a22d01229 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java @@ -77,7 +77,7 @@ public class RepoDetailsFragment extends Fragment { * repo object directly from the database. */ private Repo loadRepoDetails() { - return RepoProvider.Helper.findById(getActivity().getContentResolver(), getRepoId()); + return RepoProvider.Helper.findById(getActivity(), getRepoId()); } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -156,8 +156,7 @@ public class RepoDetailsFragment extends Fragment { name.setText(repo.getName()); - int appCount = RepoProvider.Helper.countAppsForRepo( - getActivity().getContentResolver(), repo.getId()); + int appCount = RepoProvider.Helper.countAppsForRepo(getActivity(), repo.getId()); numApps.setText(Integer.toString(appCount)); setupDescription(repoView, repo); @@ -197,7 +196,7 @@ public class RepoDetailsFragment extends Fragment { // Ensure repo is enabled before updating... ContentValues values = new ContentValues(1); values.put(RepoProvider.DataColumns.IN_USE, 1); - RepoProvider.Helper.update(getActivity().getContentResolver(), repo, values); + RepoProvider.Helper.update(getActivity(), repo, values); UpdateService.updateRepoNow(repo.address, getActivity()).setListener(new ProgressListener() { @Override @@ -230,7 +229,7 @@ public class RepoDetailsFragment extends Fragment { if (!repo.address.equals(s.toString())) { ContentValues values = new ContentValues(1); values.put(RepoProvider.DataColumns.ADDRESS, s.toString()); - RepoProvider.Helper.update(getActivity().getContentResolver(), repo, values); + RepoProvider.Helper.update(getActivity(), repo, values); } } } @@ -310,7 +309,7 @@ public class RepoDetailsFragment extends Fragment { @Override public void onClick(DialogInterface dialog, int which) { Repo repo = RepoDetailsFragment.this.repo; - RepoProvider.Helper.remove(getActivity().getContentResolver(), repo.getId()); + RepoProvider.Helper.remove(getActivity(), repo.getId()); getActivity().finish(); } }).setNegativeButton(android.R.string.cancel, diff --git a/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java index f1f0555c9..a1ef3e9fd 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java @@ -106,8 +106,7 @@ public class RepoListFragment extends ListFragment if (repo.inuse != isEnabled) { ContentValues values = new ContentValues(1); values.put(RepoProvider.DataColumns.IN_USE, isEnabled ? 1 : 0); - RepoProvider.Helper.update( - getActivity().getContentResolver(), repo, values); + RepoProvider.Helper.update(getActivity(), repo, values); if (isEnabled) { changed = true; @@ -312,7 +311,7 @@ public class RepoListFragment extends ListFragment final EditText fingerprintEditText = (EditText) view.findViewById(R.id.edit_fingerprint); final Repo repo = (newAddress != null && isImportingRepo) - ? RepoProvider.Helper.findByAddress(getActivity().getContentResolver(), newAddress) + ? RepoProvider.Helper.findByAddress(getActivity(), newAddress) : null; alrt.setIcon(android.R.drawable.ic_menu_add); @@ -409,7 +408,7 @@ public class RepoListFragment extends ListFragment ContentValues values = new ContentValues(2); values.put(RepoProvider.DataColumns.ADDRESS, address); values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint.toUpperCase(Locale.ENGLISH)); - RepoProvider.Helper.insert(getActivity().getContentResolver(), values); + RepoProvider.Helper.insert(getActivity(), values); finishedAddingRepo(); } @@ -419,7 +418,7 @@ public class RepoListFragment extends ListFragment private void createNewRepo(Repo repo) { ContentValues values = new ContentValues(1); values.put(RepoProvider.DataColumns.IN_USE, 1); - RepoProvider.Helper.update(getActivity().getContentResolver(), repo, values); + RepoProvider.Helper.update(getActivity(), repo, values); repo.inuse = true; finishedAddingRepo(); } From a4de616b7af47f8aad83254c77c45261f01de529 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 18 Feb 2014 16:41:28 -0500 Subject: [PATCH 127/282] make ApkProvider.Helper take Context rather than ContentResolver This makes the code a bit neater, and passing the Context around is a common pattern. https://dev.guardianproject.info/issues/2926 refs #2926 --- src/org/fdroid/fdroid/AppDetails.java | 2 +- src/org/fdroid/fdroid/UpdateService.java | 2 +- src/org/fdroid/fdroid/data/ApkProvider.java | 10 ++++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 330201416..bd4c17030 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -92,7 +92,7 @@ public class AppDetails extends ListActivity { public ApkListAdapter(Context context, App app) { super(context, 0); - List apks = ApkProvider.Helper.findByApp(context.getContentResolver(), app.id); + List apks = ApkProvider.Helper.findByApp(context, app.id); for (Apk apk : apks ) { if (apk.compatible || pref_incompatibleVersions) { add(apk); diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index 6f71da16b..7baf088ac 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -545,7 +545,7 @@ public class UpdateService extends IntentService implements ProgressListener { ApkProvider.DataColumns.VERSION, ApkProvider.DataColumns.VERSION_CODE }; - return ApkProvider.Helper.knownApks(getContentResolver(), apks, fields); + return ApkProvider.Helper.knownApks(this, apks, fields); } private void updateOrInsertApps(List appsToUpdate, int totalUpdateCount, int currentCount) { diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java index 6c9afbdeb..e86deb5a1 100644 --- a/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -111,12 +111,13 @@ public class ApkProvider extends FDroidProvider { resolver.delete(uri, null, null); } - public static List findByApp(ContentResolver resolver, String appId) { - return findByApp(resolver, appId, ApkProvider.DataColumns.ALL); + public static List findByApp(Context context, String appId) { + return findByApp(context, appId, ApkProvider.DataColumns.ALL); } - public static List findByApp(ContentResolver resolver, + public static List findByApp(Context context, String appId, String[] projection) { + ContentResolver resolver = context.getContentResolver(); Uri uri = getAppUri(appId); String sort = ApkProvider.DataColumns.VERSION_CODE + " DESC"; Cursor cursor = resolver.query(uri, projection, null, null, sort); @@ -127,8 +128,9 @@ public class ApkProvider extends FDroidProvider { * Returns apks in the database, which have the same id and version as * one of the apks in the "apks" argument. */ - public static List knownApks(ContentResolver resolver, + public static List knownApks(Context context, List apks, String[] fields) { + ContentResolver resolver = context.getContentResolver(); Uri uri = getContentUri(apks); Cursor cursor = resolver.query(uri, fields, null, null, null); return cursorToList(cursor); From dc1bdc2f3b19d3b6acb0b9b06140ae6c9e4a54d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 19 Feb 2014 18:07:34 +0100 Subject: [PATCH 128/282] Make the incompatible reasons textview stand out In other words, don't disable the view with the others when marking an apk as incompatible --- src/org/fdroid/fdroid/AppDetails.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index bd4c17030..2976fd462 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -191,7 +191,6 @@ public class AppDetails extends ListActivity { holder.status, holder.size, holder.api, - holder.incompatibleReasons, holder.buildtype, holder.added, holder.nativecode From e6ec5ee2420aa4148130da9c7eca62a3e176f261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 19 Feb 2014 18:11:32 +0100 Subject: [PATCH 129/282] Add support for filtering apk compatibility by maxSdkVersion For now it's enforced like minSdkVersion. It is possible to try and install incompatible apks by enabling "Incompatible Versions" and agreeing to the warning shown when clicking on such a version. --- res/values/strings.xml | 4 +++- src/org/fdroid/fdroid/AppDetails.java | 15 ++++++++++++--- src/org/fdroid/fdroid/CompatibilityChecker.java | 4 ++-- src/org/fdroid/fdroid/RepoXMLHandler.java | 6 ++++++ src/org/fdroid/fdroid/compat/Compatibility.java | 6 ++++++ src/org/fdroid/fdroid/data/Apk.java | 4 ++++ src/org/fdroid/fdroid/data/ApkProvider.java | 7 ++++--- src/org/fdroid/fdroid/data/DBHelper.java | 3 ++- 8 files changed, 39 insertions(+), 10 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 680cd375d..e0708d046 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -200,7 +200,9 @@ Disabled "%1$s".\n\nYou will need to re-enable this repository to install apps from it. - Android %s or later + %s or later + up to %s + %1$s up to %2$s Your device is not on the same WiFi as the local repo you just added! Try joining this network: %s Requires: %1$s diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 2976fd462..0cafa5e91 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -145,12 +145,21 @@ public class AppDetails extends ListActivity { holder.size.setVisibility(View.GONE); } - if (pref_expert && apk.minSdkVersion > 0) { + if (!pref_expert) { + holder.api.setVisibility(View.GONE); + } else if (apk.minSdkVersion > 0 && apk.maxSdkVersion > 0) { + holder.api.setText(getString(R.string.minsdk_up_to_maxsdk, + Utils.getAndroidVersionName(apk.minSdkVersion), + Utils.getAndroidVersionName(apk.maxSdkVersion))); + holder.api.setVisibility(View.VISIBLE); + } else if (apk.minSdkVersion > 0) { holder.api.setText(getString(R.string.minsdk_or_later, Utils.getAndroidVersionName(apk.minSdkVersion))); holder.api.setVisibility(View.VISIBLE); - } else { - holder.api.setVisibility(View.GONE); + } else if (apk.maxSdkVersion > 0) { + holder.api.setText(getString(R.string.up_to_maxsdk, + Utils.getAndroidVersionName(apk.maxSdkVersion))); + holder.api.setVisibility(View.VISIBLE); } if (apk.srcname != null) { diff --git a/src/org/fdroid/fdroid/CompatibilityChecker.java b/src/org/fdroid/fdroid/CompatibilityChecker.java index 5aa4ac31c..c17565120 100644 --- a/src/org/fdroid/fdroid/CompatibilityChecker.java +++ b/src/org/fdroid/fdroid/CompatibilityChecker.java @@ -69,7 +69,7 @@ public class CompatibilityChecker extends Compatibility { List incompatibleReasons = new ArrayList(); - if (!hasApi(apk.minSdkVersion)) { + if (!hasApi(apk.minSdkVersion) || !upToApi(apk.maxSdkVersion)) { incompatibleReasons.add( context.getResources().getString( R.string.minsdk_or_later, @@ -100,4 +100,4 @@ public class CompatibilityChecker extends Compatibility { return incompatibleReasons; } -} \ No newline at end of file +} diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java index 5f0e2e046..a123c70ed 100644 --- a/src/org/fdroid/fdroid/RepoXMLHandler.java +++ b/src/org/fdroid/fdroid/RepoXMLHandler.java @@ -158,6 +158,12 @@ public class RepoXMLHandler extends DefaultHandler { } catch (NumberFormatException ex) { curapk.minSdkVersion = 0; } + } else if (curel.equals("maxsdkver")) { + try { + curapk.maxSdkVersion = Integer.parseInt(str); + } catch (NumberFormatException ex) { + curapk.maxSdkVersion = 0; + } } else if (curel.equals("added")) { try { curapk.added = str.length() == 0 ? null : Utils.DATE_FORMAT diff --git a/src/org/fdroid/fdroid/compat/Compatibility.java b/src/org/fdroid/fdroid/compat/Compatibility.java index 5fe507bba..388c7e085 100644 --- a/src/org/fdroid/fdroid/compat/Compatibility.java +++ b/src/org/fdroid/fdroid/compat/Compatibility.java @@ -4,10 +4,16 @@ import android.os.Build; public abstract class Compatibility { + // like minSdkVersion protected static boolean hasApi(int apiLevel) { return getApi() >= apiLevel; } + // like maxSdkVersion + protected static boolean upToApi(int apiLevel) { + return (apiLevel < 1 || getApi() <= apiLevel); + } + protected static int getApi() { return Build.VERSION.SDK_INT; } diff --git a/src/org/fdroid/fdroid/data/Apk.java b/src/org/fdroid/fdroid/data/Apk.java index 9f6b305f1..95ff15f65 100644 --- a/src/org/fdroid/fdroid/data/Apk.java +++ b/src/org/fdroid/fdroid/data/Apk.java @@ -16,6 +16,7 @@ public class Apk extends ValueObject implements Comparable { public String hash; public String hashType; public int minSdkVersion; // 0 if unknown + public int maxSdkVersion; // 0 if none public Date added; public Utils.CommaSeparatedList permissions; // null if empty or // unknown @@ -75,6 +76,8 @@ public class Apk extends ValueObject implements Comparable { compatible = cursor.getInt(i) == 1; } else if (column.equals(ApkProvider.DataColumns.MIN_SDK_VERSION)) { minSdkVersion = cursor.getInt(i); + } else if (column.equals(ApkProvider.DataColumns.MAX_SDK_VERSION)) { + maxSdkVersion = cursor.getInt(i); } else if (column.equals(ApkProvider.DataColumns.NAME)) { apkName = cursor.getString(i); } else if (column.equals(ApkProvider.DataColumns.PERMISSIONS)) { @@ -121,6 +124,7 @@ public class Apk extends ValueObject implements Comparable { values.put(ApkProvider.DataColumns.SIZE, size); values.put(ApkProvider.DataColumns.NAME, apkName); values.put(ApkProvider.DataColumns.MIN_SDK_VERSION, minSdkVersion); + values.put(ApkProvider.DataColumns.MAX_SDK_VERSION, maxSdkVersion); values.put(ApkProvider.DataColumns.ADDED_DATE, added == null ? "" : Utils.DATE_FORMAT.format(added)); values.put(ApkProvider.DataColumns.PERMISSIONS, Utils.CommaSeparatedList.str(permissions)); values.put(ApkProvider.DataColumns.FEATURES, Utils.CommaSeparatedList.str(features)); diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java index e86deb5a1..8e7be5ed9 100644 --- a/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -149,6 +149,7 @@ public class ApkProvider extends FDroidProvider { public static String SIGNATURE = "sig"; public static String SOURCE_NAME = "srcname"; public static String MIN_SDK_VERSION = "minSdkVersion"; + public static String MAX_SDK_VERSION = "maxSdkVersion"; public static String PERMISSIONS = "permissions"; public static String FEATURES = "features"; public static String NATIVE_CODE = "nativecode"; @@ -161,9 +162,9 @@ public class ApkProvider extends FDroidProvider { public static String[] ALL = { _ID, APK_ID, VERSION, REPO_ID, HASH, VERSION_CODE, NAME, SIZE, - SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, PERMISSIONS, FEATURES, - NATIVE_CODE, HASH_TYPE, ADDED_DATE, IS_COMPATIBLE, - REPO_VERSION, REPO_ADDRESS, INCOMPATIBLE_REASONS + SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, MAX_SDK_VERSION, + PERMISSIONS, FEATURES, NATIVE_CODE, HASH_TYPE, ADDED_DATE, + IS_COMPATIBLE, REPO_VERSION, REPO_ADDRESS, INCOMPATIBLE_REASONS }; } diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index df872d2c9..db273fa29 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -46,6 +46,7 @@ public class DBHelper extends SQLiteOpenHelper { + "sig string, " + "srcname string, " + "minSdkVersion integer, " + + "maxSdkVersion integer, " + "permissions string, " + "features string, " + "nativecode string, " @@ -86,7 +87,7 @@ public class DBHelper extends SQLiteOpenHelper { + "iconUrl text, " + "primary key(id));"; - private static final int DB_VERSION = 39; + private static final int DB_VERSION = 40; private Context context; From 8bb0e58e6c57c7415d08b065e807bdc546cf52ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 19 Feb 2014 18:23:31 +0100 Subject: [PATCH 130/282] Update changelog and todo --- CHANGELOG.md | 2 ++ TODO-before-release.md | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 790b0b3b9..9875f74ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ * Support for TLS Subject-Public-Key-Identifier pinning +* Filter app compatibility by maxSdkVersion too + * Various fixes to layout issues introduced in 0.58 ### 0.58 (2014-01-11) diff --git a/TODO-before-release.md b/TODO-before-release.md index ced948976..1677a8bee 100644 --- a/TODO-before-release.md +++ b/TODO-before-release.md @@ -3,3 +3,9 @@ These issues are a must-fix before the next stable release: * Right after updating a repo, "Recently Updated" shows the apps correctly but the new apks don't show up on App Details until the whole app is restarted (or until the repos are wiped and re-downloaded) + +* App.curVersion is now used in some places where before we used + App.curApk.version, which means that e.g. app lists now show the current + version at upstream and not the latest stable version in the repository + (highly misleading to users, who might end up looking for versions not in + the repo yet) From 3223e20e33f20c3180c19079253cdc888f3c2768 Mon Sep 17 00:00:00 2001 From: Daniel McCarney Date: Wed, 19 Feb 2014 14:52:01 -0500 Subject: [PATCH 131/282] Add support for Network Service Discovery of FDroid repos. If the device supports API level 16 (Android 4.1) then add a menu item on the repository management screen to "Find Local Repos". Activating this menu item will initiate NSD service discovery with the NsdHelper class looking for 'fdroidrepo' and 'fdroidrepos' service types on the local network. When one is found, the service is resolved and the name & IP are populated into a list of discovered repositories. Clicking an NSD discovered repo will prompt the user to add the repo. --- CHANGELOG.md | 3 + res/layout/repodiscoveryitem.xml | 27 ++ res/layout/repodiscoverylist.xml | 43 +++ res/values/strings.xml | 4 + src/org/fdroid/fdroid/net/NsdHelper.java | 251 ++++++++++++++++++ .../views/fragments/RepoListFragment.java | 76 +++++- 6 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 res/layout/repodiscoveryitem.xml create mode 100644 res/layout/repodiscoverylist.xml create mode 100644 src/org/fdroid/fdroid/net/NsdHelper.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 9875f74ad..2aa8d411b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ### Upcoming release +* Support for Network Service Discovery of local FDroid repos on Android 4.1+ + from the repository management screen. + * Always remember the selected category in the list of apps * Send FDroid via Bluetooth to any device that supports receiving APKs via diff --git a/res/layout/repodiscoveryitem.xml b/res/layout/repodiscoveryitem.xml new file mode 100644 index 000000000..333b5e9c7 --- /dev/null +++ b/res/layout/repodiscoveryitem.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/res/layout/repodiscoverylist.xml b/res/layout/repodiscoverylist.xml new file mode 100644 index 000000000..077636d55 --- /dev/null +++ b/res/layout/repodiscoverylist.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index e0708d046..54b027945 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -100,6 +100,7 @@ Search New Repository Remove Repository + Find Local Repos Run Share @@ -148,6 +149,9 @@ What\'s New Recently Updated + Local FDroid Repos + Discovering local FDroid repos… + From 8f506c0b0b047d2510f60fa3d73327a2cb3f7e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 20 Feb 2014 00:25:14 +0100 Subject: [PATCH 135/282] Add maxage issue to TODO --- TODO-before-release.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/TODO-before-release.md b/TODO-before-release.md index 1677a8bee..d499bf676 100644 --- a/TODO-before-release.md +++ b/TODO-before-release.md @@ -1,11 +1,14 @@ These issues are a must-fix before the next stable release: -* Right after updating a repo, "Recently Updated" shows the apps correctly but +* Fix updating of `TABLE_REPO` from a very old version, also known as the + `duplicate column name: maxage` issue. + +* Right after updating a repo, `Recently Updated` shows the apps correctly but the new apks don't show up on App Details until the whole app is restarted (or until the repos are wiped and re-downloaded) -* App.curVersion is now used in some places where before we used - App.curApk.version, which means that e.g. app lists now show the current +* `App.curVersion` is now used in some places where before we used + `App.curApk.version`, which means that e.g. app lists now show the current version at upstream and not the latest stable version in the repository (highly misleading to users, who might end up looking for versions not in the repo yet) From 5d828e234111449d41e2e0528f808905c0177df2 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 19 Feb 2014 19:52:36 -0500 Subject: [PATCH 136/282] remove all unused imports This reduces the number of warnings so that we can see the useful ones! --- src/org/fdroid/fdroid/AppFilter.java | 3 --- src/org/fdroid/fdroid/Preferences.java | 1 - src/org/fdroid/fdroid/Utils.java | 3 --- src/org/fdroid/fdroid/compat/ClipboardCompat.java | 4 ---- src/org/fdroid/fdroid/compat/SupportedArchitectures.java | 1 - src/org/fdroid/fdroid/compat/SwitchCompat.java | 1 - src/org/fdroid/fdroid/data/DBHelper.java | 2 -- src/org/fdroid/fdroid/views/AppListAdapter.java | 1 - src/org/fdroid/fdroid/views/fragments/AppListFragment.java | 5 ----- .../fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java | 4 ---- .../fdroid/fdroid/views/fragments/InstalledAppsFragment.java | 4 ---- test/src/org/fdroid/fdroid/ApkProviderTest.java | 2 +- test/src/org/fdroid/fdroid/AppProviderTest.java | 3 ++- test/src/org/fdroid/fdroid/TestUtils.java | 1 - 14 files changed, 3 insertions(+), 32 deletions(-) diff --git a/src/org/fdroid/fdroid/AppFilter.java b/src/org/fdroid/fdroid/AppFilter.java index 24f723d70..29d0731fe 100644 --- a/src/org/fdroid/fdroid/AppFilter.java +++ b/src/org/fdroid/fdroid/AppFilter.java @@ -18,9 +18,6 @@ package org.fdroid.fdroid; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; import org.fdroid.fdroid.data.App; public class AppFilter { diff --git a/src/org/fdroid/fdroid/Preferences.java b/src/org/fdroid/fdroid/Preferences.java index 47eebdc72..4b984deeb 100644 --- a/src/org/fdroid/fdroid/Preferences.java +++ b/src/org/fdroid/fdroid/Preferences.java @@ -2,7 +2,6 @@ package org.fdroid.fdroid; import java.util.*; -import android.app.LoaderManager; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; diff --git a/src/org/fdroid/fdroid/Utils.java b/src/org/fdroid/fdroid/Utils.java index 97f4e4a92..6f2950aac 100644 --- a/src/org/fdroid/fdroid/Utils.java +++ b/src/org/fdroid/fdroid/Utils.java @@ -26,7 +26,6 @@ import android.util.DisplayMetrics; import android.util.Log; import com.nostra13.universalimageloader.utils.StorageUtils; -import java.io.BufferedReader; import java.io.Closeable; import java.io.File; import java.io.FileReader; @@ -39,8 +38,6 @@ import java.text.SimpleDateFormat; import java.security.MessageDigest; import java.util.*; -import org.fdroid.fdroid.data.Repo; - public final class Utils { public static final int BUFFER_SIZE = 4096; diff --git a/src/org/fdroid/fdroid/compat/ClipboardCompat.java b/src/org/fdroid/fdroid/compat/ClipboardCompat.java index 8e4c49cbc..11a8a7786 100644 --- a/src/org/fdroid/fdroid/compat/ClipboardCompat.java +++ b/src/org/fdroid/fdroid/compat/ClipboardCompat.java @@ -4,10 +4,6 @@ import android.annotation.TargetApi; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.widget.CompoundButton; -import android.widget.Switch; -import android.widget.ToggleButton; -import org.fdroid.fdroid.ManageRepo; public abstract class ClipboardCompat extends Compatibility { diff --git a/src/org/fdroid/fdroid/compat/SupportedArchitectures.java b/src/org/fdroid/fdroid/compat/SupportedArchitectures.java index b28708d5a..cc88a534b 100644 --- a/src/org/fdroid/fdroid/compat/SupportedArchitectures.java +++ b/src/org/fdroid/fdroid/compat/SupportedArchitectures.java @@ -4,7 +4,6 @@ import java.util.Set; import java.util.HashSet; import android.annotation.TargetApi; -import android.util.Log; import android.os.Build; public class SupportedArchitectures extends Compatibility { diff --git a/src/org/fdroid/fdroid/compat/SwitchCompat.java b/src/org/fdroid/fdroid/compat/SwitchCompat.java index b5cbb7815..3177fe601 100644 --- a/src/org/fdroid/fdroid/compat/SwitchCompat.java +++ b/src/org/fdroid/fdroid/compat/SwitchCompat.java @@ -2,7 +2,6 @@ package org.fdroid.fdroid.compat; import android.annotation.TargetApi; import android.content.Context; -import android.os.Build; import android.widget.CompoundButton; import android.widget.Switch; import android.widget.ToggleButton; diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index db273fa29..3696b5f91 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -2,11 +2,9 @@ package org.fdroid.fdroid.data; import android.content.ContentValues; import android.content.Context; -import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import android.preference.PreferenceManager; import android.util.Log; import org.fdroid.fdroid.*; diff --git a/src/org/fdroid/fdroid/views/AppListAdapter.java b/src/org/fdroid/fdroid/views/AppListAdapter.java index 3665ec53e..5659283fd 100644 --- a/src/org/fdroid/fdroid/views/AppListAdapter.java +++ b/src/org/fdroid/fdroid/views/AppListAdapter.java @@ -5,7 +5,6 @@ import android.content.pm.PackageInfo; import android.database.Cursor; import android.graphics.Bitmap; import android.support.v4.widget.CursorAdapter; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java index 523c271e7..3ad2c08a3 100644 --- a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java @@ -1,23 +1,18 @@ package org.fdroid.fdroid.views.fragments; -import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.preference.PreferenceManager; import android.support.v4.app.ListFragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.util.Log; import android.view.View; -import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.ListView; - import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.listener.PauseOnScrollListener; diff --git a/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java index 0241b3657..d42cbd6c2 100644 --- a/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java @@ -1,10 +1,6 @@ package org.fdroid.fdroid.views.fragments; -import android.database.Cursor; import android.net.Uri; -import android.os.Bundle; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.views.AppListAdapter; diff --git a/src/org/fdroid/fdroid/views/fragments/InstalledAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/InstalledAppsFragment.java index dbcdf5cbb..8585786b0 100644 --- a/src/org/fdroid/fdroid/views/fragments/InstalledAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/InstalledAppsFragment.java @@ -1,10 +1,6 @@ package org.fdroid.fdroid.views.fragments; -import android.database.Cursor; import android.net.Uri; -import android.os.Bundle; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.views.AppListAdapter; diff --git a/test/src/org/fdroid/fdroid/ApkProviderTest.java b/test/src/org/fdroid/fdroid/ApkProviderTest.java index 4c808a066..6c065c814 100644 --- a/test/src/org/fdroid/fdroid/ApkProviderTest.java +++ b/test/src/org/fdroid/fdroid/ApkProviderTest.java @@ -3,9 +3,9 @@ package org.fdroid.fdroid; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; + import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; -import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.mock.MockApk; import org.fdroid.fdroid.mock.MockApp; diff --git a/test/src/org/fdroid/fdroid/AppProviderTest.java b/test/src/org/fdroid/fdroid/AppProviderTest.java index 9c1c696a0..6b801869d 100644 --- a/test/src/org/fdroid/fdroid/AppProviderTest.java +++ b/test/src/org/fdroid/fdroid/AppProviderTest.java @@ -2,9 +2,10 @@ package org.fdroid.fdroid; import android.content.ContentValues; import android.database.Cursor; -import android.net.Uri; + import mock.MockCategoryResources; import mock.MockInstallablePackageManager; + import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppProvider; diff --git a/test/src/org/fdroid/fdroid/TestUtils.java b/test/src/org/fdroid/fdroid/TestUtils.java index 420c32c45..b61822a41 100644 --- a/test/src/org/fdroid/fdroid/TestUtils.java +++ b/test/src/org/fdroid/fdroid/TestUtils.java @@ -2,7 +2,6 @@ package org.fdroid.fdroid; import android.content.*; import android.net.Uri; -import android.test.mock.MockContentResolver; import junit.framework.AssertionFailedError; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.AppProvider; From 3a0d40d86d8a420aebd540ece9ed5fa853c2260f Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 19 Feb 2014 19:53:38 -0500 Subject: [PATCH 137/282] remove all unused variables This reduces the number of warnings so that we can see the useful ones! --- src/org/fdroid/fdroid/FDroidApp.java | 7 ------- src/org/fdroid/fdroid/UpdateService.java | 1 - src/org/fdroid/fdroid/data/ApkProvider.java | 2 +- src/org/fdroid/fdroid/data/AppProvider.java | 2 +- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/org/fdroid/fdroid/FDroidApp.java b/src/org/fdroid/fdroid/FDroidApp.java index 13f01829b..c3fa5b717 100644 --- a/src/org/fdroid/fdroid/FDroidApp.java +++ b/src/org/fdroid/fdroid/FDroidApp.java @@ -49,7 +49,6 @@ import com.nostra13.universalimageloader.utils.StorageUtils; import de.duenndns.ssl.MemorizingTrustManager; -import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppProvider; import org.thoughtcrime.ssl.pinning.PinningTrustManager; import org.thoughtcrime.ssl.pinning.SystemKeyStore; @@ -124,7 +123,6 @@ public class FDroidApp extends Application { } } - apps = null; invalidApps = new ArrayList(); ctx = getApplicationContext(); UpdateService.schedule(ctx); @@ -187,12 +185,8 @@ public class FDroidApp extends Application { private Context ctx; - // Global list of all known applications. - private List apps; - // Set when something has changed (database or installed apps) so we know // we should invalidate the apps. - private volatile boolean appsAllInvalid = false; private Semaphore appsInvalidLock = new Semaphore(1, false); private List invalidApps; @@ -201,7 +195,6 @@ public class FDroidApp extends Application { public void invalidateAllApps() { try { appsInvalidLock.acquire(); - appsAllInvalid = true; } catch (InterruptedException e) { // Don't care } finally { diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index 7baf088ac..dc3aa0711 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -247,7 +247,6 @@ public class UpdateService extends IntentService implements ProgressListener { // Grab some preliminary information, then we can release the // database while we do all the downloading, etc... - int updates = 0; List repos = RepoProvider.Helper.all(this); // Process each repo... diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java index 8e7be5ed9..27987908e 100644 --- a/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -442,7 +442,7 @@ public class ApkProvider extends FDroidProvider { public Uri insert(Uri uri, ContentValues values) { removeRepoFields(values); validateFields(DataColumns.ALL, values); - long id = write().insertOrThrow(getTableName(), null, values); + write().insertOrThrow(getTableName(), null, values); if (!isApplyingBatch()) { getContext().getContentResolver().notifyChange(uri, null); } diff --git a/src/org/fdroid/fdroid/data/AppProvider.java b/src/org/fdroid/fdroid/data/AppProvider.java index c931fa2bd..0ce0b1ac0 100644 --- a/src/org/fdroid/fdroid/data/AppProvider.java +++ b/src/org/fdroid/fdroid/data/AppProvider.java @@ -458,7 +458,7 @@ public class AppProvider extends FDroidProvider { @Override public Uri insert(Uri uri, ContentValues values) { - long id = write().insertOrThrow(getTableName(), null, values); + write().insertOrThrow(getTableName(), null, values); if (!isApplyingBatch()) { getContext().getContentResolver().notifyChange(uri, null); } From 301ac105159190081f6cf9bad335e86d9549faf5 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 19 Feb 2014 19:59:31 -0500 Subject: [PATCH 138/282] remove trailing white space... --- src/org/fdroid/fdroid/AppFilter.java | 2 +- src/org/fdroid/fdroid/FDroidApp.java | 10 ++++++---- src/org/fdroid/fdroid/FDroidCertPins.java | 4 ++-- src/org/fdroid/fdroid/updater/RepoUpdater.java | 2 +- test/src/org/fdroid/fdroid/ApkProviderTest.java | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/org/fdroid/fdroid/AppFilter.java b/src/org/fdroid/fdroid/AppFilter.java index 29d0731fe..ad5479cf2 100644 --- a/src/org/fdroid/fdroid/AppFilter.java +++ b/src/org/fdroid/fdroid/AppFilter.java @@ -29,7 +29,7 @@ public class AppFilter { boolean dontFilterRequiringRoot = Preferences.get().filterAppsRequiringRoot(); if (app.requirements == null || dontFilterRequiringRoot) return false; - + for (String r : app.requirements) { if (r.equals("root")) return true; diff --git a/src/org/fdroid/fdroid/FDroidApp.java b/src/org/fdroid/fdroid/FDroidApp.java index c3fa5b717..f51978b9c 100644 --- a/src/org/fdroid/fdroid/FDroidApp.java +++ b/src/org/fdroid/fdroid/FDroidApp.java @@ -58,6 +58,7 @@ public class FDroidApp extends Application { private static enum Theme { dark, light } + private static Theme curTheme = Theme.dark; public void reloadTheme() { @@ -65,6 +66,7 @@ public class FDroidApp extends Application { .getDefaultSharedPreferences(getBaseContext()) .getString(Preferences.PREF_THEME, "dark")); } + public void applyTheme(Activity activity) { switch (curTheme) { case dark: @@ -147,16 +149,16 @@ public class FDroidApp extends Application { try { SSLContext sc = SSLContext.getInstance("TLS"); X509TrustManager defaultTrustManager = null; - + /* * init a trust manager factory with a null keystore to access the system trust managers */ - TrustManagerFactory tmf = + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); KeyStore ks = null; tmf.init(ks); TrustManager[] mgrs = tmf.getTrustManagers(); - + if(mgrs.length > 0 && mgrs[0] instanceof X509TrustManager) defaultTrustManager = (X509TrustManager) mgrs[0]; @@ -166,7 +168,7 @@ public class FDroidApp extends Application { */ PinningTrustManager pinMgr = new PinningTrustManager(SystemKeyStore.getInstance(ctx),FDroidCertPins.getPinList(), 0); MemorizingTrustManager memMgr = new MemorizingTrustManager(ctx, pinMgr, defaultTrustManager); - + /* * initialize a SSLContext with the outermost trust manager, use this * context to set the default SSL socket factory for the HTTPSURLConnection diff --git a/src/org/fdroid/fdroid/FDroidCertPins.java b/src/org/fdroid/fdroid/FDroidCertPins.java index 9ecf61847..98b36ec35 100644 --- a/src/org/fdroid/fdroid/FDroidCertPins.java +++ b/src/org/fdroid/fdroid/FDroidCertPins.java @@ -31,8 +31,8 @@ public class FDroidCertPins { * SPKI Pin: 638F93856E1F5EDFCBD40C46D4160CFF21B0713A */ "638F93856E1F5EDFCBD40C46D4160CFF21B0713A", - - /* + + /* * SubjectDN: CN=guardianproject.info, OU=Gandi Standard SSL, OU=Domain Control Validated * IssuerDN: CN=Gandi Standard SSL CA, O=GANDI SAS, C=FR * Fingerprint: 187C2573E924DFCBFF2A781A2F99D71C6E031828 diff --git a/src/org/fdroid/fdroid/updater/RepoUpdater.java b/src/org/fdroid/fdroid/updater/RepoUpdater.java index 2096a2db8..00003d01d 100644 --- a/src/org/fdroid/fdroid/updater/RepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/RepoUpdater.java @@ -248,7 +248,7 @@ abstract public class RepoUpdater { + repo.version + " to " + handler.getVersion()); values.put(RepoProvider.DataColumns.VERSION, handler.getVersion()); } - + if (handler.getMaxAge() != -1 && handler.getMaxAge() != repo.maxage) { Log.d("FDroid", "Repo specified a new maximum age - updated"); diff --git a/test/src/org/fdroid/fdroid/ApkProviderTest.java b/test/src/org/fdroid/fdroid/ApkProviderTest.java index 6c065c814..bd265ba54 100644 --- a/test/src/org/fdroid/fdroid/ApkProviderTest.java +++ b/test/src/org/fdroid/fdroid/ApkProviderTest.java @@ -231,7 +231,7 @@ public class ApkProviderTest extends FDroidProviderTest { ApkProvider.DataColumns.APK_ID, ApkProvider.DataColumns.VERSION_CODE }; - + Cursor cursor = getMockContentResolver().query(uri, projections, null, null, null); cursor.moveToFirst(); Apk apk = new Apk(cursor); From 888d28aed6c63737207c5502db520305fc7e3599 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 19 Feb 2014 20:01:39 -0500 Subject: [PATCH 139/282] @Override decorator on every method that overrides This marks a method as overriding another method, and makes sure that it matches the signature of the method it is supposed to be overriding, otherwise it gives a warning. Its a bit verbose, but can catch mistakes and save time. And the default Android profile for Eclipse always adds them automatically... --- src/org/fdroid/fdroid/ProgressListener.java | 2 ++ src/org/fdroid/fdroid/compat/TabManager.java | 7 +++++++ src/org/fdroid/fdroid/data/ApkProvider.java | 2 ++ src/org/fdroid/fdroid/data/AppProvider.java | 1 + src/org/fdroid/fdroid/data/Repo.java | 1 + src/org/fdroid/fdroid/data/RepoProvider.java | 1 + src/org/fdroid/fdroid/net/NsdHelper.java | 1 + src/org/fdroid/fdroid/updater/SignedRepoUpdater.java | 1 + src/org/fdroid/fdroid/views/RepoAdapter.java | 1 + .../fdroid/views/fragments/AvailableAppsFragment.java | 1 + .../fdroid/fdroid/views/fragments/RepoDetailsFragment.java | 4 ++++ test/src/org/fdroid/fdroid/ApkProviderTest.java | 1 + test/src/org/fdroid/fdroid/AppProviderTest.java | 1 + 13 files changed, 24 insertions(+) diff --git a/src/org/fdroid/fdroid/ProgressListener.java b/src/org/fdroid/fdroid/ProgressListener.java index ab83d08b0..5550c1492 100644 --- a/src/org/fdroid/fdroid/ProgressListener.java +++ b/src/org/fdroid/fdroid/ProgressListener.java @@ -66,10 +66,12 @@ public interface ProgressListener { } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override public Event createFromParcel(Parcel in) { return new Event(in.readInt(), in.readInt(), in.readInt(), in.readBundle()); } + @Override public Event[] newArray(int size) { return new Event[size]; } diff --git a/src/org/fdroid/fdroid/compat/TabManager.java b/src/org/fdroid/fdroid/compat/TabManager.java index e3edeb6da..d93e9fdf3 100644 --- a/src/org/fdroid/fdroid/compat/TabManager.java +++ b/src/org/fdroid/fdroid/compat/TabManager.java @@ -66,6 +66,7 @@ class OldTabManagerImpl extends TabManager { * and giving it a FrameLayout as a child. This will make the tabs have * dummy empty contents and then hook them up to our ViewPager. */ + @Override public void createTabs() { tabHost = new TabHost(parent, null); tabHost.setLayoutParams(new TabHost.LayoutParams( @@ -128,12 +129,14 @@ class OldTabManagerImpl extends TabManager { } + @Override public void selectTab(int index) { tabHost.setCurrentTab(index); if (index == INDEX_CAN_UPDATE) removeNotification(1); } + @Override public void refreshTabLabel(int index) { CharSequence text = getLabel(index); @@ -166,6 +169,7 @@ class HoneycombTabManagerImpl extends TabManager { actionBar = parent.getActionBar(); } + @Override public void createTabs() { actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); for (int i = 0; i < pager.getAdapter().getCount(); i ++) { @@ -174,6 +178,7 @@ class HoneycombTabManagerImpl extends TabManager { actionBar.newTab() .setText(label) .setTabListener(new ActionBar.TabListener() { + @Override public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) { int pos = tab.getPosition(); @@ -193,6 +198,7 @@ class HoneycombTabManagerImpl extends TabManager { } } + @Override public void selectTab(int index) { actionBar.setSelectedNavigationItem(index); Spinner actionBarSpinner = getActionBarSpinner(); @@ -203,6 +209,7 @@ class HoneycombTabManagerImpl extends TabManager { removeNotification(1); } + @Override public void refreshTabLabel(int index) { CharSequence text = getLabel(index); actionBar.getTabAt(index).setText(text); diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java index 27987908e..34efab702 100644 --- a/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -255,6 +255,7 @@ public class ApkProvider extends FDroidProvider { return PROVIDER_NAME; } + @Override protected UriMatcher getMatcher() { return matcher; } @@ -323,6 +324,7 @@ public class ApkProvider extends FDroidProvider { this.orderBy = orderBy; } + @Override public String toString() { StringBuilder suffix = new StringBuilder(); diff --git a/src/org/fdroid/fdroid/data/AppProvider.java b/src/org/fdroid/fdroid/data/AppProvider.java index 0ce0b1ac0..f404df0c0 100644 --- a/src/org/fdroid/fdroid/data/AppProvider.java +++ b/src/org/fdroid/fdroid/data/AppProvider.java @@ -266,6 +266,7 @@ public class AppProvider extends FDroidProvider { return AUTHORITY + "." + PROVIDER_NAME; } + @Override protected UriMatcher getMatcher() { return matcher; } diff --git a/src/org/fdroid/fdroid/data/Repo.java b/src/org/fdroid/fdroid/data/Repo.java index 1b0bdd055..76001ca1d 100644 --- a/src/org/fdroid/fdroid/data/Repo.java +++ b/src/org/fdroid/fdroid/data/Repo.java @@ -72,6 +72,7 @@ public class Repo extends ValueObject { return name; } + @Override public String toString() { return address; } diff --git a/src/org/fdroid/fdroid/data/RepoProvider.java b/src/org/fdroid/fdroid/data/RepoProvider.java index 9d23b440d..530ce8ecf 100644 --- a/src/org/fdroid/fdroid/data/RepoProvider.java +++ b/src/org/fdroid/fdroid/data/RepoProvider.java @@ -234,6 +234,7 @@ public class RepoProvider extends FDroidProvider { return "RepoProvider"; } + @Override protected UriMatcher getMatcher() { return matcher; } diff --git a/src/org/fdroid/fdroid/net/NsdHelper.java b/src/org/fdroid/fdroid/net/NsdHelper.java index de5c67763..b56dbe37c 100644 --- a/src/org/fdroid/fdroid/net/NsdHelper.java +++ b/src/org/fdroid/fdroid/net/NsdHelper.java @@ -202,6 +202,7 @@ public class NsdHelper { //in order for it to update the ListView without error Handler refresh = new Handler(Looper.getMainLooper()); refresh.post(new Runnable() { + @Override public void run() { notifyDataSetChanged(); diff --git a/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java b/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java index ce22fd0c3..9b5ef8b1e 100644 --- a/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java @@ -84,6 +84,7 @@ public class SignedRepoUpdater extends RepoUpdater { return indexFile; } + @Override protected String getIndexAddress() { return repo.address + "/index.jar?client_version=" + context.getString(R.string.version_name); } diff --git a/src/org/fdroid/fdroid/views/RepoAdapter.java b/src/org/fdroid/fdroid/views/RepoAdapter.java index 3fcb3c3aa..c3cd8d60e 100644 --- a/src/org/fdroid/fdroid/views/RepoAdapter.java +++ b/src/org/fdroid/fdroid/views/RepoAdapter.java @@ -45,6 +45,7 @@ public class RepoAdapter extends CursorAdapter { enabledListener = listener; } + @Override public boolean hasStableIds() { return true; } diff --git a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java index ee8efedd3..d222440c4 100644 --- a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java @@ -37,6 +37,7 @@ public class AvailableAppsFragment extends AppListFragment implements return "Available"; } + @Override protected AppListAdapter getAppListAdapter() { if (adapter == null) { final AppListAdapter a = new AvailableAppListAdapter(getActivity(), null); diff --git a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java index a22d01229..0a91d8748 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java @@ -62,6 +62,7 @@ public class RepoDetailsFragment extends Fragment { // best way to go about this... private Repo repo; + @Override public void onAttach(Activity activity) { super.onAttach(activity); } @@ -80,6 +81,7 @@ public class RepoDetailsFragment extends Fragment { return RepoProvider.Helper.findById(getActivity(), getRepoId()); } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { repo = loadRepoDetails(); @@ -346,11 +348,13 @@ public class RepoDetailsFragment extends Fragment { repoFingerprintView.setTextColor(repoFingerprintColor); } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); } + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); } diff --git a/test/src/org/fdroid/fdroid/ApkProviderTest.java b/test/src/org/fdroid/fdroid/ApkProviderTest.java index bd265ba54..cee69d733 100644 --- a/test/src/org/fdroid/fdroid/ApkProviderTest.java +++ b/test/src/org/fdroid/fdroid/ApkProviderTest.java @@ -20,6 +20,7 @@ public class ApkProviderTest extends FDroidProviderTest { super(ApkProvider.class, ApkProvider.getAuthority()); } + @Override protected String[] getMinimalProjection() { return new String[] { ApkProvider.DataColumns.APK_ID, diff --git a/test/src/org/fdroid/fdroid/AppProviderTest.java b/test/src/org/fdroid/fdroid/AppProviderTest.java index 6b801869d..2768e858b 100644 --- a/test/src/org/fdroid/fdroid/AppProviderTest.java +++ b/test/src/org/fdroid/fdroid/AppProviderTest.java @@ -25,6 +25,7 @@ public class AppProviderTest extends FDroidProviderTest { getSwappableContext().setResources(new MockCategoryResources()); } + @Override protected String[] getMinimalProjection() { return new String[] { AppProvider.DataColumns.APP_ID, From dd3562c00fbcac9f5dd133df366909664e45dd4e Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 19 Feb 2014 20:02:29 -0500 Subject: [PATCH 140/282] remove unnecessary cast This is pretty cosmetic, but Eclipse did it for me, so why not? :-) --- src/org/fdroid/fdroid/PreferencesActivity.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/org/fdroid/fdroid/PreferencesActivity.java b/src/org/fdroid/fdroid/PreferencesActivity.java index 8de8a4589..569f3dc73 100644 --- a/src/org/fdroid/fdroid/PreferencesActivity.java +++ b/src/org/fdroid/fdroid/PreferencesActivity.java @@ -152,8 +152,7 @@ public class PreferencesActivity extends PreferenceActivity implements super.onResume(); getPreferenceScreen().getSharedPreferences() - .registerOnSharedPreferenceChangeListener( - (OnSharedPreferenceChangeListener)this); + .registerOnSharedPreferenceChangeListener(this); for (String key : summariesToUpdate) { updateSummary(key, false); From 27874b3a9e0faa9027e276016bca2e08e191abf1 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 19 Feb 2014 20:05:28 -0500 Subject: [PATCH 141/282] parameterize CategoryObserver.adapter Helps Java do its error checking... and gets rid of a few warnings... --- .../fdroid/fdroid/views/fragments/AvailableAppsFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java index d222440c4..98f70d35a 100644 --- a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java @@ -54,7 +54,7 @@ public class AvailableAppsFragment extends AppListFragment implements private class CategoryObserver extends ContentObserver { - private ArrayAdapter adapter; + private ArrayAdapter adapter; public CategoryObserver(ArrayAdapter adapter) { super(null); From 576208d3aa2095a3da7bcc65b06acad8a19b5d04 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 19 Feb 2014 20:11:30 -0500 Subject: [PATCH 142/282] Exception subclasses are supposed to have a serial number This warning in Eclipse tells me so: "The serializable class UpdateException does not declare a static final serialVersionUID field of type long" --- src/org/fdroid/fdroid/updater/RepoUpdater.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/fdroid/fdroid/updater/RepoUpdater.java b/src/org/fdroid/fdroid/updater/RepoUpdater.java index 00003d01d..d961e26fb 100644 --- a/src/org/fdroid/fdroid/updater/RepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/RepoUpdater.java @@ -268,6 +268,7 @@ abstract public class RepoUpdater { public static class UpdateException extends Exception { + private static final long serialVersionUID = -4492452418826132803L; public final Repo repo; public UpdateException(Repo repo, String message) { From 3240faf7f2c92a2ba4f077b11209107c96cb29bb Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 20 Feb 2014 15:29:50 +1100 Subject: [PATCH 143/282] Fix "duplicate column: maxage" (issue #445) The bug is explained in detail in the issue tracker. This change added guard condition to check for existence of the field before adding. While I was at it, I also guarded a bunch of other ALTER statements with the if (!columnExists()) check. It turns out that many of them break, but we only saw the first one because it threw an exception before getting to the others. --- src/org/fdroid/fdroid/data/DBHelper.java | 76 ++++++++++++------------ 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index db273fa29..8600e47ce 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -120,49 +120,49 @@ public class DBHelper extends SQLiteOpenHelper { } private void renameRepoId(SQLiteDatabase db, int oldVersion) { - if (oldVersion < 36) { + if (oldVersion < 36 && !columnExists(db, TABLE_REPO, "_id")) { Log.d("FDroid", "Renaming " + TABLE_REPO + ".id to _id"); db.beginTransaction(); try { - // http://stackoverflow.com/questions/805363/how-do-i-rename-a-column-in-a-sqlite-database-table#805508 - String tempTableName = TABLE_REPO + "__temp__"; - db.execSQL("ALTER TABLE " + TABLE_REPO + " RENAME TO " + tempTableName + ";" ); + // http://stackoverflow.com/questions/805363/how-do-i-rename-a-column-in-a-sqlite-database-table#805508 + String tempTableName = TABLE_REPO + "__temp__"; + db.execSQL("ALTER TABLE " + TABLE_REPO + " RENAME TO " + tempTableName + ";" ); - // I realise this is available in the CREATE_TABLE_REPO above, - // however I have a feeling that it will need to be the same as the - // current structure of the table as of DBVersion 36, or else we may - // get into strife. For example, if there was a field that - // got removed, then it will break the "insert select" - // statement. Therefore, I've put a copy of CREATE_TABLE_REPO - // here that is the same as it was at DBVersion 36. - String createTableDdl = "create table " + TABLE_REPO + " (" - + "_id integer not null primary key, " - + "address text not null, " - + "name text, " - + "description text, " - + "inuse integer not null, " - + "priority integer not null, " - + "pubkey text, " - + "fingerprint text, " - + "maxage integer not null default 0, " - + "version integer not null default 0, " - + "lastetag text, " - + "lastUpdated string);"; + // I realise this is available in the CREATE_TABLE_REPO above, + // however I have a feeling that it will need to be the same as the + // current structure of the table as of DBVersion 36, or else we may + // get into strife. For example, if there was a field that + // got removed, then it will break the "insert select" + // statement. Therefore, I've put a copy of CREATE_TABLE_REPO + // here that is the same as it was at DBVersion 36. + String createTableDdl = "create table " + TABLE_REPO + " (" + + "_id integer not null primary key, " + + "address text not null, " + + "name text, " + + "description text, " + + "inuse integer not null, " + + "priority integer not null, " + + "pubkey text, " + + "fingerprint text, " + + "maxage integer not null default 0, " + + "version integer not null default 0, " + + "lastetag text, " + + "lastUpdated string);"; - db.execSQL(createTableDdl); + db.execSQL(createTableDdl); - String nonIdFields = "address, name, description, inuse, priority, " + - "pubkey, fingerprint, maxage, version, lastetag, lastUpdated"; + String nonIdFields = "address, name, description, inuse, priority, " + + "pubkey, fingerprint, maxage, version, lastetag, lastUpdated"; - String insertSql = "INSERT INTO " + TABLE_REPO + - "(_id, " + nonIdFields + " ) " + - "SELECT id, " + nonIdFields + " FROM " + tempTableName + ";"; + String insertSql = "INSERT INTO " + TABLE_REPO + + "(_id, " + nonIdFields + " ) " + + "SELECT id, " + nonIdFields + " FROM " + tempTableName + ";"; - db.execSQL(insertSql); - db.execSQL("DROP TABLE " + tempTableName + ";"); - db.setTransactionSuccessful(); + db.execSQL(insertSql); + db.execSQL("DROP TABLE " + tempTableName + ";"); + db.setTransactionSuccessful(); } catch (Exception e) { Log.e("FDroid", "Error renaming id to _id: " + e.getMessage()); } @@ -273,10 +273,12 @@ public class DBHelper extends SQLiteOpenHelper { * default repos with values from strings.xml. */ private void addNameAndDescriptionToRepo(SQLiteDatabase db, int oldVersion) { - if (oldVersion < 21) { - if (!columnExists(db, TABLE_REPO, "name")) + boolean nameExists = columnExists(db, TABLE_REPO, "name"); + boolean descriptionExists = columnExists(db, TABLE_REPO, "description"); + if (oldVersion < 21 && !(nameExists && descriptionExists)) { + if (!nameExists) db.execSQL("alter table " + TABLE_REPO + " add column name text"); - if (!columnExists(db, TABLE_REPO, "description")) + if (!descriptionExists) db.execSQL("alter table " + TABLE_REPO + " add column description text"); ContentValues values = new ContentValues(); values.put("name", context.getString(R.string.default_repo_name)); @@ -322,7 +324,7 @@ public class DBHelper extends SQLiteOpenHelper { } private void addMaxAgeToRepo(SQLiteDatabase db, int oldVersion) { - if (oldVersion < 30) { + if (oldVersion < 30 && !columnExists(db, TABLE_REPO, "maxage")) { db.execSQL("alter table " + TABLE_REPO + " add column maxage integer not null default 0"); } } From 51a21595592a420791836f44244bc99d176beee1 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 20 Feb 2014 16:01:27 +1100 Subject: [PATCH 144/282] Removed maxage issue from TODO --- TODO-before-release.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/TODO-before-release.md b/TODO-before-release.md index d499bf676..e635a2463 100644 --- a/TODO-before-release.md +++ b/TODO-before-release.md @@ -1,8 +1,5 @@ These issues are a must-fix before the next stable release: -* Fix updating of `TABLE_REPO` from a very old version, also known as the - `duplicate column name: maxage` issue. - * Right after updating a repo, `Recently Updated` shows the apps correctly but the new apks don't show up on App Details until the whole app is restarted (or until the repos are wiped and re-downloaded) From 2dcd87cd410b60a0f705d23d2bdc33c7472ea776 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 20 Feb 2014 07:16:09 +1100 Subject: [PATCH 145/282] Almost 100% test coverage of ApkProvider and ApkProvider.Helper Removed unused code from ApkProvider.Helper, made it throw proper exceptions when trying unsupported operations. Refactored tests a little bit to facilitate separate test cases for the provider and its helper. --- src/org/fdroid/fdroid/data/ApkProvider.java | 63 +----- .../fdroid/fdroid/ApkProviderHelperTest.java | 202 ++++++++++++++++++ .../org/fdroid/fdroid/ApkProviderTest.java | 141 +++++------- .../fdroid/fdroid/BaseApkProviderTest.java | 76 +++++++ .../org/fdroid/fdroid/FDroidProviderTest.java | 18 ++ test/src/org/fdroid/fdroid/TestUtils.java | 8 +- 6 files changed, 369 insertions(+), 139 deletions(-) create mode 100644 test/src/org/fdroid/fdroid/ApkProviderHelperTest.java create mode 100644 test/src/org/fdroid/fdroid/BaseApkProviderTest.java diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java index 8e7be5ed9..f111f39d9 100644 --- a/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -25,45 +25,12 @@ public class ApkProvider extends FDroidProvider { private Helper() {} - public static void update(Context context, Apk apk, - String id, int versionCode) { - ContentResolver resolver = context.getContentResolver(); - Uri uri = getContentUri(id, versionCode); - resolver.update(uri, apk.toContentValues(), null, null); - } - public static void update(Context context, Apk apk) { ContentResolver resolver = context.getContentResolver(); Uri uri = getContentUri(apk.id, apk.vercode); resolver.update(uri, apk.toContentValues(), null, null); } - /** - * This doesn't do anything other than call "insert" on the content - * resolver, but I thought I'd put it here in the interests of having - * each of the CRUD methods available in the helper class. - */ - public static void insert(Context context, ContentValues values) { - ContentResolver resolver = context.getContentResolver(); - resolver.insert(getContentUri(), values); - } - - public static void insert(Context context, Apk apk) { - insert(context, apk.toContentValues()); - } - - public static List all(Context context) { - return all(context, DataColumns.ALL); - } - - public static List all(Context context, String[] projection) { - - ContentResolver resolver = context.getContentResolver(); - Uri uri = ApkProvider.getContentUri(); - Cursor cursor = resolver.query(uri, projection, null, null, null); - return cursorToList(cursor); - } - public static List cursorToList(Cursor cursor) { List apks = new ArrayList(); if (cursor != null) { @@ -105,12 +72,6 @@ public class ApkProvider extends FDroidProvider { } } - public static void delete(Context context, String id, int versionCode) { - ContentResolver resolver = context.getContentResolver(); - Uri uri = getContentUri(id, versionCode); - resolver.delete(uri, null, null); - } - public static List findByApp(Context context, String appId) { return findByApp(context, appId, ApkProvider.DataColumns.ALL); } @@ -139,7 +100,7 @@ public class ApkProvider extends FDroidProvider { public interface DataColumns extends BaseColumns { - public static String APK_ID = "id"; + public static String APK_ID = "id"; public static String VERSION = "version"; public static String REPO_ID = "repo"; public static String HASH = "hash"; @@ -273,8 +234,8 @@ public class ApkProvider extends FDroidProvider { addRepoField(REPO_FIELDS.get(field), field); } else if (field.equals(DataColumns._ID)) { appendField("rowid", "apk", "_id"); - } else if (field.startsWith("COUNT")) { - appendField(field); + } else if (field.equals(DataColumns._COUNT)) { + appendField("COUNT(*) AS " + DataColumns._COUNT); } else { appendField(field, "apk"); } @@ -496,20 +457,16 @@ public class ApkProvider extends FDroidProvider { @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { - QuerySelection query = new QuerySelection(where, whereArgs); - - validateFields(DataColumns.ALL, values); - - switch (matcher.match(uri)) { - case CODE_LIST: - return 0; - - case CODE_SINGLE: - query = query.add(querySingle(uri)); - break; + if (matcher.match(uri) != CODE_SINGLE) { + throw new UnsupportedOperationException("Cannot update anything other than a single apk."); } + validateFields(DataColumns.ALL, values); removeRepoFields(values); + + QuerySelection query = new QuerySelection(where, whereArgs); + query = query.add(querySingle(uri)); + int numRows = write().update(getTableName(), values, query.getSelection(), query.getArgs()); if (!isApplyingBatch()) { getContext().getContentResolver().notifyChange(uri, null); diff --git a/test/src/org/fdroid/fdroid/ApkProviderHelperTest.java b/test/src/org/fdroid/fdroid/ApkProviderHelperTest.java new file mode 100644 index 000000000..678c9903f --- /dev/null +++ b/test/src/org/fdroid/fdroid/ApkProviderHelperTest.java @@ -0,0 +1,202 @@ +package org.fdroid.fdroid; + +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.ApkProvider; +import org.fdroid.fdroid.mock.MockApk; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +public class ApkProviderHelperTest extends BaseApkProviderTest { + + public void testKnownApks() { + + for (int i = 0; i < 7; i ++) + TestUtils.insertApk(this, "org.fdroid.fdroid", i); + + for (int i = 0; i < 9; i ++) + TestUtils.insertApk(this, "org.example", i); + + for (int i = 0; i < 3; i ++) + TestUtils.insertApk(this, "com.example", i); + + TestUtils.insertApk(this, "com.apk.thingo", 1); + + Apk[] known = { + new MockApk("org.fdroid.fdroid", 1), + new MockApk("org.fdroid.fdroid", 3), + new MockApk("org.fdroid.fdroid", 5), + + new MockApk("com.example", 1), + new MockApk("com.example", 2), + }; + + Apk[] unknown = { + new MockApk("org.fdroid.fdroid", 7), + new MockApk("org.fdroid.fdroid", 9), + new MockApk("org.fdroid.fdroid", 11), + new MockApk("org.fdroid.fdroid", 13), + + new MockApk("com.example", 3), + new MockApk("com.example", 4), + new MockApk("com.example", 5), + + new MockApk("info.example", 1), + new MockApk("info.example", 2), + }; + + List apksToCheck = new ArrayList(known.length + unknown.length); + Collections.addAll(apksToCheck, known); + Collections.addAll(apksToCheck, unknown); + + String[] projection = { + ApkProvider.DataColumns.APK_ID, + ApkProvider.DataColumns.VERSION_CODE + }; + + List knownApks = ApkProvider.Helper.knownApks(getMockContext(), apksToCheck, projection); + + assertResultCount(known.length, knownApks); + + for (Apk knownApk : knownApks) + assertContains(knownApks, knownApk); + } + + public void testFindByApp() { + + for (int i = 0; i < 7; i ++) + TestUtils.insertApk(this, "org.fdroid.fdroid", i); + + for (int i = 0; i < 9; i ++) + TestUtils.insertApk(this, "org.example", i); + + for (int i = 0; i < 3; i ++) + TestUtils.insertApk(this, "com.example", i); + + TestUtils.insertApk(this, "com.apk.thingo", 1); + + assertTotalApkCount(7 + 9 + 3 + 1); + + List fdroidApks = ApkProvider.Helper.findByApp(getMockContext(), "org.fdroid.fdroid"); + assertResultCount(7, fdroidApks); + assertBelongsToApp(fdroidApks, "org.fdroid.fdroid"); + + List exampleApks = ApkProvider.Helper.findByApp(getMockContext(), "org.example"); + assertResultCount(9, exampleApks); + assertBelongsToApp(exampleApks, "org.example"); + + List exampleApks2 = ApkProvider.Helper.findByApp(getMockContext(), "com.example"); + assertResultCount(3, exampleApks2); + assertBelongsToApp(exampleApks2, "com.example"); + + List thingoApks = ApkProvider.Helper.findByApp(getMockContext(), "com.apk.thingo"); + assertResultCount(1, thingoApks); + assertBelongsToApp(thingoApks, "com.apk.thingo"); + } + + public void testUpdate() { + + Uri apkUri = TestUtils.insertApk(this, "com.example", 10); + + String[] allFields = ApkProvider.DataColumns.ALL; + Cursor cursor = getMockContentResolver().query(apkUri, allFields, null, null, null); + assertResultCount(1, cursor); + + cursor.moveToFirst(); + Apk apk = new Apk(cursor); + + assertEquals("com.example", apk.id); + assertEquals(10, apk.vercode); + + assertNull(apk.features); + assertNull(apk.added); + assertNull(apk.hashType); + + apk.features = Utils.CommaSeparatedList.make("one,two,three"); + long dateTimestamp = System.currentTimeMillis(); + apk.added = new Date(dateTimestamp); + apk.hashType = "i'm a hash type"; + + ApkProvider.Helper.update(getMockContext(), apk); + + // Should not have inserted anything else, just updated the already existing apk. + Cursor allCursor = getMockContentResolver().query(ApkProvider.getContentUri(), allFields, null, null, null); + assertResultCount(1, allCursor); + + Cursor updatedCursor = getMockContentResolver().query(apkUri, allFields, null, null, null); + assertResultCount(1, updatedCursor); + + updatedCursor.moveToFirst(); + Apk updatedApk = new Apk(updatedCursor); + + assertEquals("com.example", updatedApk.id); + assertEquals(10, updatedApk.vercode); + + assertNotNull(updatedApk.features); + assertNotNull(updatedApk.added); + assertNotNull(updatedApk.hashType); + + assertEquals("one,two,three", updatedApk.features.toString()); + assertEquals(new Date(dateTimestamp).getYear(), updatedApk.added.getYear()); + assertEquals(new Date(dateTimestamp).getMonth(), updatedApk.added.getMonth()); + assertEquals(new Date(dateTimestamp).getDay(), updatedApk.added.getDay()); + assertEquals("i'm a hash type", updatedApk.hashType); + } + + public void testFind() { + + // Insert some random apks either side of the "com.example", so that + // the Helper.find() method doesn't stumble upon the app we are interested + // in by shear dumb luck... + for (int i = 0; i < 10; i ++) + TestUtils.insertApk(this, "org.fdroid.apk." + i, i); + + ContentValues values = new ContentValues(); + values.put(ApkProvider.DataColumns.VERSION, "v1.1"); + values.put(ApkProvider.DataColumns.HASH, "xxxxyyyy"); + values.put(ApkProvider.DataColumns.HASH_TYPE, "a hash type"); + TestUtils.insertApk(this, "com.example", 11, values); + + // ...and a few more for good measure... + for (int i = 15; i < 20; i ++) + TestUtils.insertApk(this, "com.other.thing." + i, i); + + Apk apk = ApkProvider.Helper.find(getMockContext(), "com.example", 11); + + assertNotNull(apk); + + // The find() method populates ALL fields if you don't specify any, + // so we expect to find each of the ones we inserted above... + assertEquals("com.example", apk.id); + assertEquals(11, apk.vercode); + assertEquals("v1.1", apk.version); + assertEquals("xxxxyyyy", apk.hash); + assertEquals("a hash type", apk.hashType); + + String[] projection = { + ApkProvider.DataColumns.APK_ID, + ApkProvider.DataColumns.HASH + }; + + Apk apkLessFields = ApkProvider.Helper.find(getMockContext(), "com.example", 11, projection); + + assertNotNull(apkLessFields); + + assertEquals("com.example", apkLessFields.id); + assertEquals("xxxxyyyy", apkLessFields.hash); + + // Didn't ask for these fields, so should be their default values... + assertNull(apkLessFields.hashType); + assertNull(apkLessFields.version); + assertEquals(0, apkLessFields.vercode); + + Apk notFound = ApkProvider.Helper.find(getMockContext(), "com.doesnt.exist", 1000); + assertNull(notFound); + } + +} diff --git a/test/src/org/fdroid/fdroid/ApkProviderTest.java b/test/src/org/fdroid/fdroid/ApkProviderTest.java index 4c808a066..c6e1d3055 100644 --- a/test/src/org/fdroid/fdroid/ApkProviderTest.java +++ b/test/src/org/fdroid/fdroid/ApkProviderTest.java @@ -5,7 +5,6 @@ import android.database.Cursor; import android.net.Uri; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; -import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.mock.MockApk; import org.fdroid.fdroid.mock.MockApp; @@ -14,20 +13,7 @@ import org.fdroid.fdroid.mock.MockRepo; import java.util.ArrayList; import java.util.List; -public class ApkProviderTest extends FDroidProviderTest { - - public ApkProviderTest() { - super(ApkProvider.class, ApkProvider.getAuthority()); - } - - protected String[] getMinimalProjection() { - return new String[] { - ApkProvider.DataColumns.APK_ID, - ApkProvider.DataColumns.VERSION_CODE, - ApkProvider.DataColumns.NAME, - ApkProvider.DataColumns.REPO_ID - }; - } +public class ApkProviderTest extends BaseApkProviderTest { public void testUris() { assertInvalidUri(ApkProvider.getAuthority()); @@ -70,8 +56,8 @@ public class ApkProviderTest extends FDroidProviderTest { public void testAppApks() { for (int i = 1; i <= 10; i ++) { - insertApk("org.fdroid.fdroid", i); - insertApk("com.example", i); + TestUtils.insertApk(this, "org.fdroid.fdroid", i); + TestUtils.insertApk(this, "com.example", i); } assertTotalApkCount(20); @@ -97,20 +83,35 @@ public class ApkProviderTest extends FDroidProviderTest { assertBelongsToApp(all, "org.fdroid.fdroid"); } - public void testInvalidDeleteUris() { - assertCantDelete(ApkProvider.getContentUri()); - assertCantDelete(ApkProvider.getContentUri(new ArrayList())); - assertCantDelete(ApkProvider.getContentUri("org.fdroid.fdroid", 10)); - assertCantDelete(ApkProvider.getContentUri(new MockApk("org.fdroid.fdroid", 10))); + public void testInvalidUpdateUris() { + Apk apk = new MockApk("org.fdroid.fdroid", 10); - try { - getMockContentResolver().delete(RepoProvider.getContentUri(), null, null); - fail(); - } catch (IllegalArgumentException e) { - // Don't fail, it is what we were looking for... - } catch (Exception e) { - fail(); - } + List apks = new ArrayList(); + apks.add(apk); + + assertCantUpdate(ApkProvider.getContentUri()); + assertCantUpdate(ApkProvider.getAppUri("org.fdroid.fdroid")); + assertCantUpdate(ApkProvider.getRepoUri(1)); + assertCantUpdate(ApkProvider.getContentUri(apks)); + assertCantUpdate(Uri.withAppendedPath(ApkProvider.getContentUri(), "some-random-path")); + + // The only valid ones are: + // ApkProvider.getContentUri(apk) + // ApkProvider.getContentUri(id, version) + // which are tested elsewhere. + } + + public void testInvalidDeleteUris() { + Apk apk = new MockApk("org.fdroid.fdroid", 10); + + List apks = new ArrayList(); + apks.add(apk); + + assertCantDelete(ApkProvider.getContentUri()); + assertCantDelete(ApkProvider.getContentUri(apks)); + assertCantDelete(ApkProvider.getContentUri("org.fdroid.fdroid", 10)); + assertCantDelete(ApkProvider.getContentUri(apk)); + assertCantDelete(Uri.withAppendedPath(ApkProvider.getContentUri(), "some-random-path")); } public void testRepoApks() { @@ -163,7 +164,7 @@ public class ApkProviderTest extends FDroidProviderTest { Apk apk = new MockApk("org.fdroid.fdroid", 13); // Insert a new record... - Uri newUri = insertApk(apk.id, apk.vercode); + Uri newUri = TestUtils.insertApk(this, apk.id, apk.vercode); assertEquals(ApkProvider.getContentUri(apk).toString(), newUri.toString()); cursor = queryAllApks(); assertNotNull(cursor); @@ -189,6 +190,26 @@ public class ApkProviderTest extends FDroidProviderTest { assertEquals(13, toCheck.vercode); } + public void testCount() { + String[] projectionFields = getMinimalProjection(); + String[] projectionCount = new String[] { ApkProvider.DataColumns._COUNT }; + + for (int i = 0; i < 13; i ++) { + TestUtils.insertApk(this, "com.example", i); + } + + Uri all = ApkProvider.getContentUri(); + Cursor allWithFields = getMockContentResolver().query(all, projectionFields, null, null, null); + Cursor allWithCount = getMockContentResolver().query(all, projectionCount, null, null, null); + + assertResultCount(13, allWithFields); + assertResultCount(1, allWithCount); + + allWithCount.moveToFirst(); + int countColumn = allWithCount.getColumnIndex(ApkProvider.DataColumns._COUNT); + assertEquals(13, allWithCount.getInt(countColumn)); + } + public void testInsertWithExtraFields() { assertResultCount(0, queryAllApks()); @@ -205,7 +226,7 @@ public class ApkProviderTest extends FDroidProviderTest { ContentValues invalidRepo = new ContentValues(); invalidRepo.put(field, "Test data"); try { - insertApk("org.fdroid.fdroid", 10, invalidRepo); + TestUtils.insertApk(this, "org.fdroid.fdroid", 10, invalidRepo); fail(); } catch (IllegalArgumentException e) { } catch (Exception e) { @@ -219,24 +240,17 @@ public class ApkProviderTest extends FDroidProviderTest { values.put(ApkProvider.DataColumns.REPO_ADDRESS, "http://example.com"); values.put(ApkProvider.DataColumns.REPO_VERSION, 3); values.put(ApkProvider.DataColumns.FEATURES, "Some features"); - Uri uri = insertApk("com.example.com", 1, values); + Uri uri = TestUtils.insertApk(this, "com.example.com", 1, values); assertResultCount(1, queryAllApks()); - String[] projections = { - ApkProvider.DataColumns.REPO_ID, - ApkProvider.DataColumns.REPO_ADDRESS, - ApkProvider.DataColumns.REPO_VERSION, - ApkProvider.DataColumns.FEATURES, - ApkProvider.DataColumns.APK_ID, - ApkProvider.DataColumns.VERSION_CODE - }; - + String[] projections = ApkProvider.DataColumns.ALL; Cursor cursor = getMockContentResolver().query(uri, projections, null, null, null); cursor.moveToFirst(); Apk apk = new Apk(cursor); - // These should have quietly been dropped when we tried to save them... + // These should have quietly been dropped when we tried to save them, + // because the provider only knows how to query them (not update them). assertEquals(null, apk.repoAddress); assertEquals(0, apk.repoVersion); @@ -247,45 +261,4 @@ public class ApkProviderTest extends FDroidProviderTest { assertEquals(10, apk.repo); } - public void testIgnore() { - /*for (int i = 0; i < 10; i ++) { - insertApk("org.fdroid.fdroid", i); - }*/ - } - - private void assertBelongsToApp(Cursor apks, String appId) { - for (Apk apk : ApkProvider.Helper.cursorToList(apks)) { - assertEquals(appId, apk.id); - } - } - - private void assertTotalApkCount(int expected) { - assertResultCount(expected, queryAllApks()); - } - - private void assertBelongsToRepo(Cursor apkCursor, long repoId) { - for (Apk apk : ApkProvider.Helper.cursorToList(apkCursor)) { - assertEquals(repoId, apk.repo); - } - } - - private void insertApkForRepo(String id, int versionCode, long repoId) { - ContentValues additionalValues = new ContentValues(); - additionalValues.put(ApkProvider.DataColumns.REPO_ID, repoId); - insertApk(id, versionCode, additionalValues); - } - - private Cursor queryAllApks() { - return getMockContentResolver().query(ApkProvider.getContentUri(), getMinimalProjection(), null, null, null); - } - - private Uri insertApk(String id, int versionCode) { - return insertApk(id, versionCode, new ContentValues()); - } - - private Uri insertApk(String id, int versionCode, - ContentValues additionalValues) { - return TestUtils.insertApk(getMockContentResolver(), id, versionCode, additionalValues); - } - } diff --git a/test/src/org/fdroid/fdroid/BaseApkProviderTest.java b/test/src/org/fdroid/fdroid/BaseApkProviderTest.java new file mode 100644 index 000000000..e42818232 --- /dev/null +++ b/test/src/org/fdroid/fdroid/BaseApkProviderTest.java @@ -0,0 +1,76 @@ +package org.fdroid.fdroid; + +import android.content.ContentValues; +import android.database.Cursor; +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.ApkProvider; + +import java.util.List; + +/** + * Provides helper methods that can be used by both Helper and plain old + * Provider tests. Allows the test classes to contain only test methods, + * hopefully making them easier to understand. + * + * This should not contain any test methods, or else they get executed + * once for every concrete subclass. + */ +abstract class BaseApkProviderTest extends FDroidProviderTest { + + public BaseApkProviderTest() { + super(ApkProvider.class, ApkProvider.getAuthority()); + } + + @Override + protected String[] getMinimalProjection() { + return new String[] { + ApkProvider.DataColumns.APK_ID, + ApkProvider.DataColumns.VERSION_CODE, + ApkProvider.DataColumns.NAME, + ApkProvider.DataColumns.REPO_ID + }; + } + + protected final Cursor queryAllApks() { + return getMockContentResolver().query(ApkProvider.getContentUri(), getMinimalProjection(), null, null, null); + } + + protected void assertContains(List apks, Apk apk) { + boolean found = false; + for (Apk a : apks) { + if (a.vercode == apk.vercode && a.id.equals(apk.id)) { + found = true; + break; + } + } + if (!found) { + fail("Apk [" + apk + "] not found in " + TestUtils.listToString(apks)); + } + } + + protected void assertBelongsToApp(Cursor apks, String appId) { + assertBelongsToApp(ApkProvider.Helper.cursorToList(apks), appId); + } + + protected void assertBelongsToApp(List apks, String appId) { + for (Apk apk : apks) { + assertEquals(appId, apk.id); + } + } + + protected void assertTotalApkCount(int expected) { + assertResultCount(expected, queryAllApks()); + } + + protected void assertBelongsToRepo(Cursor apkCursor, long repoId) { + for (Apk apk : ApkProvider.Helper.cursorToList(apkCursor)) { + assertEquals(repoId, apk.repo); + } + } + + protected void insertApkForRepo(String id, int versionCode, long repoId) { + ContentValues additionalValues = new ContentValues(); + additionalValues.put(ApkProvider.DataColumns.REPO_ID, repoId); + TestUtils.insertApk(this, id, versionCode, additionalValues); + } +} diff --git a/test/src/org/fdroid/fdroid/FDroidProviderTest.java b/test/src/org/fdroid/fdroid/FDroidProviderTest.java index b7497463b..7a4c4a6d6 100644 --- a/test/src/org/fdroid/fdroid/FDroidProviderTest.java +++ b/test/src/org/fdroid/fdroid/FDroidProviderTest.java @@ -1,6 +1,7 @@ package org.fdroid.fdroid; import android.annotation.TargetApi; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; @@ -12,6 +13,8 @@ import mock.MockContextSwappableComponents; import org.fdroid.fdroid.data.FDroidProvider; import org.fdroid.fdroid.mock.MockInstalledApkCache; +import java.util.List; + public abstract class FDroidProviderTest extends ProviderTestCase2MockContext { private MockContextSwappableComponents swappableContext; @@ -60,6 +63,16 @@ public abstract class FDroidProviderTest extends Provi } } + protected void assertCantUpdate(Uri uri) { + try { + getMockContentResolver().update(uri, new ContentValues(), null, null); + fail(); + } catch (UnsupportedOperationException e) { + } catch (Exception e) { + fail(); + } + } + protected void assertInvalidUri(String uri) { assertInvalidUri(Uri.parse(uri)); } @@ -96,6 +109,11 @@ public abstract class FDroidProviderTest extends Provi assertResultCount(expectedCount, cursor); } + protected void assertResultCount(int expectedCount, List items) { + assertNotNull(items); + assertEquals(expectedCount, items.size()); + } + protected void assertResultCount(int expectedCount, Cursor result) { assertNotNull(result); assertEquals(expectedCount, result.getCount()); diff --git a/test/src/org/fdroid/fdroid/TestUtils.java b/test/src/org/fdroid/fdroid/TestUtils.java index 420c32c45..d1e06fe9b 100644 --- a/test/src/org/fdroid/fdroid/TestUtils.java +++ b/test/src/org/fdroid/fdroid/TestUtils.java @@ -82,7 +82,11 @@ public class TestUtils { resolver.insert(uri, values); } - public static Uri insertApk(ContentResolver resolver, String id, int versionCode, ContentValues additionalValues) { + public static Uri insertApk(FDroidProviderTest providerTest, String id, int versionCode) { + return insertApk(providerTest, id, versionCode, new ContentValues()); + } + + public static Uri insertApk(FDroidProviderTest providerTest, String id, int versionCode, ContentValues additionalValues) { ContentValues values = new ContentValues(); @@ -101,6 +105,6 @@ public class TestUtils { Uri uri = ApkProvider.getContentUri(); - return resolver.insert(uri, values); + return providerTest.getMockContentResolver().insert(uri, values); } } From 28d5456e72af8d0c3dc3e1233703e2867e98c149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 20 Feb 2014 09:32:43 +0100 Subject: [PATCH 146/282] Update submodules --- extern/UniversalImageLoader | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/UniversalImageLoader b/extern/UniversalImageLoader index af4873eb8..2de3eb620 160000 --- a/extern/UniversalImageLoader +++ b/extern/UniversalImageLoader @@ -1 +1 @@ -Subproject commit af4873eb8b76237c80e3aee2c62fff437a2fd7d0 +Subproject commit 2de3eb620e4ca4b57cfd1593f70f88cfa48a9584 From 93ea5ea9fb018147d3ee1e1c76fad5c27e62d23a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 20 Feb 2014 09:55:54 +0100 Subject: [PATCH 147/282] Try to get grade working again Now it fails because it thinks that ":extern" is a subproject --- build.gradle | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index 405a07503..aac83cc10 100644 --- a/build.gradle +++ b/build.gradle @@ -16,12 +16,31 @@ dependencies { compile project(':extern:MemorizingTrustManager') } -project(':extern:UniversalImageLoader:library') { +subprojects { + buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:0.8.+' + } + } apply plugin: 'android-library' + android { + buildToolsVersion '19.0.1' + packagingOptions { + exclude "META-INF/LICENSE*" + exclude "META-INF/NOTICE*" + exclude "META-INF/README*" + exclude "META-INF/CHANGELOG*" + exclude "META-INF/BUILD*" + } + } +} +project(':extern:UniversalImageLoader:library') { android { compileSdkVersion 16 - buildToolsVersion '19.0.1' sourceSets { main { @@ -35,17 +54,9 @@ project(':extern:UniversalImageLoader:library') { } } -project(':extern:AndroidPinning') { - android { buildToolsVersion '19.0.1' } -} - -project(':extern:MemorizingTrustManager') { - android { buildToolsVersion '19.0.1' } -} - android { compileSdkVersion 19 - buildToolsVersion "19.0.1" + buildToolsVersion '19.0.1' sourceSets { main { From 44bb904ab0230c179d2fa09998ee8c64c9d84bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 20 Feb 2014 13:57:59 +0100 Subject: [PATCH 148/282] Remove obsolete dbSyncModeValues array --- res/layout/repodiscoveryitem.xml | 2 +- res/values/no_trans.xml | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/res/layout/repodiscoveryitem.xml b/res/layout/repodiscoveryitem.xml index 333b5e9c7..53c265df0 100644 --- a/res/layout/repodiscoveryitem.xml +++ b/res/layout/repodiscoveryitem.xml @@ -24,4 +24,4 @@ android:text="Repo Address" android:textSize="14sp" /> - \ No newline at end of file + diff --git a/res/values/no_trans.xml b/res/values/no_trans.xml index e58e1e2de..9e47414b7 100644 --- a/res/values/no_trans.xml +++ b/res/values/no_trans.xml @@ -22,12 +22,6 @@ 24 - - off - normal - full - - dark light From 16c34a95d3aa30ae3ae3893635547516d9ef55aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 20 Feb 2014 14:15:50 +0100 Subject: [PATCH 149/282] First attempt at supporting RTL Following the Android 4.2 changes, which explain how to add native support for RTL, I've replaced Right for End and Left for Start. Enabling RTL to see the results. --- AndroidManifest.xml | 2 +- res/layout-land/appdetails.xml | 4 ++-- res/layout/about.xml | 10 +++++----- res/layout/addrepo.xml | 2 +- res/layout/apklistitem.xml | 12 ++++++------ res/layout/appdetails.xml | 18 +++++++++--------- res/layout/applistitem.xml | 22 +++++++++++----------- res/layout/repo_item.xml | 6 +++--- res/layout/repodetails.xml | 16 ++++++++-------- res/layout/repodiscoveryitem.xml | 4 ++-- res/layout/repodiscoverylist.xml | 4 ++-- res/layout/searchresults.xml | 4 ++-- 12 files changed, 52 insertions(+), 52 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index fa6e046f1..fb848a3aa 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -44,7 +44,7 @@ android:label="@string/app_name" android:allowBackup="true" android:theme="@style/AppThemeDark" - android:supportsRtl="false" > + android:supportsRtl="true" > @@ -38,7 +38,7 @@ @@ -57,7 +57,7 @@ diff --git a/res/layout/addrepo.xml b/res/layout/addrepo.xml index 667bf02fa..4910a3663 100644 --- a/res/layout/addrepo.xml +++ b/res/layout/addrepo.xml @@ -36,7 +36,7 @@ android:id="@+id/overwrite_message" android:layout_width="match_parent" android:layout_height="wrap_content" - android:drawableLeft="@android:drawable/ic_dialog_alert" + android:drawableStart="@android:drawable/ic_dialog_alert" android:drawablePadding="20dp" android:padding="20dp" android:text="@string/repo_delete_to_overwrite" diff --git a/res/layout/apklistitem.xml b/res/layout/apklistitem.xml index 310bbd5bc..4eec65c00 100644 --- a/res/layout/apklistitem.xml +++ b/res/layout/apklistitem.xml @@ -30,7 +30,7 @@ @@ -38,7 +38,7 @@ @@ -46,22 +46,22 @@ diff --git a/res/layout/appdetails.xml b/res/layout/appdetails.xml index 9f3fcd070..612cd6512 100644 --- a/res/layout/appdetails.xml +++ b/res/layout/appdetails.xml @@ -24,7 +24,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" - android:layout_toRightOf="@id/icon" + android:layout_toEndOf="@id/icon" android:padding="5dp" android:baselineAligned="false" android:orientation="vertical" > @@ -34,10 +34,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" - android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" android:singleLine="true" android:ellipsize="end" - android:layout_marginLeft="6sp" + android:layout_marginStart="6sp" android:textSize="12sp" /> + android:layout_alignParentStart="true" + android:layout_toStartOf="@id/license" /> @@ -69,8 +69,8 @@ android:singleLine="true" android:ellipsize="end" android:textSize="12sp" - android:layout_alignParentLeft="true" - android:layout_toLeftOf="@id/categories" + android:layout_alignParentStart="true" + android:layout_toStartOf="@id/categories" android:layout_below="@id/title" /> diff --git a/res/layout/applistitem.xml b/res/layout/applistitem.xml index f01cc5ed6..941fe112a 100644 --- a/res/layout/applistitem.xml +++ b/res/layout/applistitem.xml @@ -19,9 +19,9 @@ android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:paddingLeft="5dp" - android:paddingRight="5dp" - android:layout_toRightOf="@id/icon" + android:paddingStart="5dp" + android:paddingEnd="5dp" + android:layout_toEndOf="@id/icon" android:layout_centerVertical="true" android:baselineAligned="false" > @@ -31,9 +31,9 @@ android:ellipsize="end" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="6sp" + android:layout_marginStart="6sp" android:layout_alignParentTop="true" - android:layout_alignParentRight="true" /> + android:layout_alignParentEnd="true" /> + android:layout_alignParentStart="true" + android:layout_toStartOf="@id/status" /> + android:layout_alignParentEnd="true" /> + android:layout_alignParentStart="true" + android:layout_toStartOf="@id/license" /> diff --git a/res/layout/repo_item.xml b/res/layout/repo_item.xml index 58d983d18..9c0e10951 100644 --- a/res/layout/repo_item.xml +++ b/res/layout/repo_item.xml @@ -15,7 +15,7 @@ + android:layout_alignParentStart="true" /> + android:layout_toEndOf="@id/img" + android:layout_alignParentStart="true"/> + android:paddingStart="@dimen/padding_side" + android:paddingEnd="@dimen/padding_side"> @@ -19,7 +19,7 @@ android:layout_height="wrap_content" android:layout_below="@+id/reposcanitemname" android:layout_marginTop="2dp" - android:paddingLeft="8sp" + android:paddingStart="8sp" android:maxLines="1" android:text="Repo Address" android:textSize="14sp" /> diff --git a/res/layout/repodiscoverylist.xml b/res/layout/repodiscoverylist.xml index 077636d55..0a8f46052 100644 --- a/res/layout/repodiscoverylist.xml +++ b/res/layout/repodiscoverylist.xml @@ -8,7 +8,7 @@ android:layout_width="match_parent" android:layout_height="50dp" android:layout_alignParentTop="true" - android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" android:layout_centerHorizontal="true" android:paddingTop="8sp" > @@ -18,7 +18,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:layout_marginRight="5dp" + android:layout_marginEnd="5dp" android:indeterminate="true" /> From a3024bc8377bfd0cc243511291fe124f54bbe2ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 21 Feb 2014 00:10:52 +0100 Subject: [PATCH 150/282] Don't crash RepoDetailsFragment if nfc is not available --- src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java index 0a91d8748..0a3ff5d2f 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java @@ -264,6 +264,9 @@ public class RepoDetailsFragment extends Fragment { private void prepareNfcMenuItems(Menu menu) { boolean needsEnableNfcMenuItem = false; NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity()); + if (nfcAdapter == null) { + return; + } if (Build.VERSION.SDK_INT < 16) needsEnableNfcMenuItem = !nfcAdapter.isEnabled(); else From 66563d30d961aec67c051c8735979092d395f992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 20 Feb 2014 14:15:50 +0100 Subject: [PATCH 151/282] First attempt at supporting RTL Following the Android 4.2 changes, which explain how to add native support for RTL, I've replaced Right for End and Left for Start. Enabling RTL to see the results. --- AndroidManifest.xml | 2 +- res/layout-land/appdetails.xml | 4 ++-- res/layout/about.xml | 10 +++++----- res/layout/addrepo.xml | 2 +- res/layout/apklistitem.xml | 12 ++++++------ res/layout/appdetails.xml | 18 +++++++++--------- res/layout/applistitem.xml | 22 +++++++++++----------- res/layout/repo_item.xml | 6 +++--- res/layout/repodetails.xml | 16 ++++++++-------- res/layout/repodiscoveryitem.xml | 4 ++-- res/layout/repodiscoverylist.xml | 4 ++-- res/layout/searchresults.xml | 4 ++-- 12 files changed, 52 insertions(+), 52 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index fa6e046f1..fb848a3aa 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -44,7 +44,7 @@ android:label="@string/app_name" android:allowBackup="true" android:theme="@style/AppThemeDark" - android:supportsRtl="false" > + android:supportsRtl="true" > @@ -38,7 +38,7 @@ @@ -57,7 +57,7 @@ diff --git a/res/layout/addrepo.xml b/res/layout/addrepo.xml index 667bf02fa..4910a3663 100644 --- a/res/layout/addrepo.xml +++ b/res/layout/addrepo.xml @@ -36,7 +36,7 @@ android:id="@+id/overwrite_message" android:layout_width="match_parent" android:layout_height="wrap_content" - android:drawableLeft="@android:drawable/ic_dialog_alert" + android:drawableStart="@android:drawable/ic_dialog_alert" android:drawablePadding="20dp" android:padding="20dp" android:text="@string/repo_delete_to_overwrite" diff --git a/res/layout/apklistitem.xml b/res/layout/apklistitem.xml index 310bbd5bc..4eec65c00 100644 --- a/res/layout/apklistitem.xml +++ b/res/layout/apklistitem.xml @@ -30,7 +30,7 @@ @@ -38,7 +38,7 @@ @@ -46,22 +46,22 @@ diff --git a/res/layout/appdetails.xml b/res/layout/appdetails.xml index 9f3fcd070..612cd6512 100644 --- a/res/layout/appdetails.xml +++ b/res/layout/appdetails.xml @@ -24,7 +24,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" - android:layout_toRightOf="@id/icon" + android:layout_toEndOf="@id/icon" android:padding="5dp" android:baselineAligned="false" android:orientation="vertical" > @@ -34,10 +34,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" - android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" android:singleLine="true" android:ellipsize="end" - android:layout_marginLeft="6sp" + android:layout_marginStart="6sp" android:textSize="12sp" /> + android:layout_alignParentStart="true" + android:layout_toStartOf="@id/license" /> @@ -69,8 +69,8 @@ android:singleLine="true" android:ellipsize="end" android:textSize="12sp" - android:layout_alignParentLeft="true" - android:layout_toLeftOf="@id/categories" + android:layout_alignParentStart="true" + android:layout_toStartOf="@id/categories" android:layout_below="@id/title" /> diff --git a/res/layout/applistitem.xml b/res/layout/applistitem.xml index f01cc5ed6..941fe112a 100644 --- a/res/layout/applistitem.xml +++ b/res/layout/applistitem.xml @@ -19,9 +19,9 @@ android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:paddingLeft="5dp" - android:paddingRight="5dp" - android:layout_toRightOf="@id/icon" + android:paddingStart="5dp" + android:paddingEnd="5dp" + android:layout_toEndOf="@id/icon" android:layout_centerVertical="true" android:baselineAligned="false" > @@ -31,9 +31,9 @@ android:ellipsize="end" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="6sp" + android:layout_marginStart="6sp" android:layout_alignParentTop="true" - android:layout_alignParentRight="true" /> + android:layout_alignParentEnd="true" /> + android:layout_alignParentStart="true" + android:layout_toStartOf="@id/status" /> + android:layout_alignParentEnd="true" /> + android:layout_alignParentStart="true" + android:layout_toStartOf="@id/license" /> diff --git a/res/layout/repo_item.xml b/res/layout/repo_item.xml index 58d983d18..9c0e10951 100644 --- a/res/layout/repo_item.xml +++ b/res/layout/repo_item.xml @@ -15,7 +15,7 @@ + android:layout_alignParentStart="true" /> + android:layout_toEndOf="@id/img" + android:layout_alignParentStart="true"/> + android:paddingStart="@dimen/padding_side" + android:paddingEnd="@dimen/padding_side"> @@ -19,7 +19,7 @@ android:layout_height="wrap_content" android:layout_below="@+id/reposcanitemname" android:layout_marginTop="2dp" - android:paddingLeft="8sp" + android:paddingStart="8sp" android:maxLines="1" android:text="Repo Address" android:textSize="14sp" /> diff --git a/res/layout/repodiscoverylist.xml b/res/layout/repodiscoverylist.xml index 077636d55..0a8f46052 100644 --- a/res/layout/repodiscoverylist.xml +++ b/res/layout/repodiscoverylist.xml @@ -8,7 +8,7 @@ android:layout_width="match_parent" android:layout_height="50dp" android:layout_alignParentTop="true" - android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" android:layout_centerHorizontal="true" android:paddingTop="8sp" > @@ -18,7 +18,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:layout_marginRight="5dp" + android:layout_marginEnd="5dp" android:indeterminate="true" /> From 0fba2c255e069457b448f4e32b36ceea65357e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 20 Feb 2014 23:59:17 +0100 Subject: [PATCH 152/282] Add START_OF to LayoutCompat.RelativeLayout --- src/org/fdroid/fdroid/compat/LayoutCompat.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/org/fdroid/fdroid/compat/LayoutCompat.java b/src/org/fdroid/fdroid/compat/LayoutCompat.java index 6a57ad033..a0c0b054e 100644 --- a/src/org/fdroid/fdroid/compat/LayoutCompat.java +++ b/src/org/fdroid/fdroid/compat/LayoutCompat.java @@ -14,9 +14,11 @@ public abstract class LayoutCompat extends Compatibility { private static final LayoutCompat impl = LayoutCompat.create(); + protected abstract int relativeLayoutStartOf(); protected abstract int relativeLayoutEndOf(); public static class RelativeLayout { + public static final int START_OF = impl.relativeLayoutStartOf(); public static final int END_OF = impl.relativeLayoutEndOf(); } @@ -24,6 +26,11 @@ public abstract class LayoutCompat extends Compatibility { class OldLayoutCompatImpl extends LayoutCompat { + @Override + protected int relativeLayoutStartOf() { + return android.widget.RelativeLayout.LEFT_OF; + } + @Override protected int relativeLayoutEndOf() { return android.widget.RelativeLayout.RIGHT_OF; @@ -33,6 +40,11 @@ class OldLayoutCompatImpl extends LayoutCompat { @TargetApi(17) class JellyBeanMr1LayoutCompatImpl extends LayoutCompat { + @Override + protected int relativeLayoutStartOf() { + return android.widget.RelativeLayout.START_OF; + } + @Override protected int relativeLayoutEndOf() { return android.widget.RelativeLayout.END_OF; From 5d0074d821a05fe335cf1d8011b18caea0d1d476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 21 Feb 2014 00:06:46 +0100 Subject: [PATCH 153/282] Add ALIGN_PARENT_* to LayoutCompat.RelativeLayout --- .../fdroid/fdroid/compat/LayoutCompat.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/org/fdroid/fdroid/compat/LayoutCompat.java b/src/org/fdroid/fdroid/compat/LayoutCompat.java index a0c0b054e..33f0ad574 100644 --- a/src/org/fdroid/fdroid/compat/LayoutCompat.java +++ b/src/org/fdroid/fdroid/compat/LayoutCompat.java @@ -16,10 +16,14 @@ public abstract class LayoutCompat extends Compatibility { protected abstract int relativeLayoutStartOf(); protected abstract int relativeLayoutEndOf(); + protected abstract int relativeLayoutAlignParentStart(); + protected abstract int relativeLayoutAlignParentEnd(); public static class RelativeLayout { public static final int START_OF = impl.relativeLayoutStartOf(); public static final int END_OF = impl.relativeLayoutEndOf(); + public static final int ALIGN_PARENT_START = impl.relativeLayoutAlignParentStart(); + public static final int ALIGN_PARENT_END = impl.relativeLayoutAlignParentEnd(); } } @@ -35,6 +39,16 @@ class OldLayoutCompatImpl extends LayoutCompat { protected int relativeLayoutEndOf() { return android.widget.RelativeLayout.RIGHT_OF; } + + @Override + protected int relativeLayoutAlignParentStart() { + return android.widget.RelativeLayout.ALIGN_PARENT_LEFT; + } + + @Override + protected int relativeLayoutAlignParentEnd() { + return android.widget.RelativeLayout.ALIGN_PARENT_RIGHT; + } } @TargetApi(17) @@ -49,4 +63,14 @@ class JellyBeanMr1LayoutCompatImpl extends LayoutCompat { protected int relativeLayoutEndOf() { return android.widget.RelativeLayout.END_OF; } + + @Override + protected int relativeLayoutAlignParentStart() { + return android.widget.RelativeLayout.ALIGN_PARENT_START; + } + + @Override + protected int relativeLayoutAlignParentEnd() { + return android.widget.RelativeLayout.ALIGN_PARENT_END; + } } From 2a03c51207fbe89cba442478858f38a4d9dd3d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 21 Feb 2014 00:08:06 +0100 Subject: [PATCH 154/282] Use LayoutCompat in RepoAdapter --- src/org/fdroid/fdroid/views/RepoAdapter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/org/fdroid/fdroid/views/RepoAdapter.java b/src/org/fdroid/fdroid/views/RepoAdapter.java index c3cd8d60e..307f325de 100644 --- a/src/org/fdroid/fdroid/views/RepoAdapter.java +++ b/src/org/fdroid/fdroid/views/RepoAdapter.java @@ -12,6 +12,7 @@ import android.widget.RelativeLayout; import android.widget.TextView; import org.fdroid.fdroid.R; import org.fdroid.fdroid.compat.SwitchCompat; +import org.fdroid.fdroid.compat.LayoutCompat; import org.fdroid.fdroid.data.Repo; public class RepoAdapter extends CursorAdapter { @@ -92,7 +93,7 @@ public class RepoAdapter extends CursorAdapter { nameView.setText(repo.getName()); RelativeLayout.LayoutParams nameViewLayout = (RelativeLayout.LayoutParams)nameView.getLayoutParams(); - nameViewLayout.addRule(RelativeLayout.LEFT_OF, switchView.getId()); + nameViewLayout.addRule(LayoutCompat.RelativeLayout.START_OF, switchView.getId()); // If we set the signed view to GONE instead of INVISIBLE, then the // height of each list item varies. @@ -112,7 +113,7 @@ public class RepoAdapter extends CursorAdapter { RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT ); - layout.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); + layout.addRule(LayoutCompat.RelativeLayout.ALIGN_PARENT_END); layout.addRule(RelativeLayout.CENTER_VERTICAL); switchView.setLayoutParams(layout); ((RelativeLayout)parent).addView(switchView); From 8b823cdf59c978cc9f251ea32ec7629b3323ad91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 21 Feb 2014 00:21:17 +0100 Subject: [PATCH 155/282] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aa8d411b..b9b7d0c7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ * Support for TLS Subject-Public-Key-Identifier pinning +* Add native Right-to-Left support on devices running 4.2 and later + * Filter app compatibility by maxSdkVersion too * Various fixes to layout issues introduced in 0.58 From 31aa3fcf306815004d5d011411956a9999b2d0bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 21 Feb 2014 00:32:17 +0100 Subject: [PATCH 156/282] Make titles and subtitles align properly in RTL This is just a cosmetic fix to make the RTL layout look like the normal LTR one. It is, effectively, making non-RTL text be aligned to the right. I suppose that's fine, for the sake of making it readable since we don't want it aligned to the left, breaking the layout. --- res/layout/appdetails.xml | 2 ++ res/layout/applistitem.xml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/res/layout/appdetails.xml b/res/layout/appdetails.xml index 612cd6512..9d48c5c0d 100644 --- a/res/layout/appdetails.xml +++ b/res/layout/appdetails.xml @@ -49,6 +49,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentStart="true" + android:textAlignment="viewStart" android:layout_toStartOf="@id/license" /> diff --git a/res/layout/applistitem.xml b/res/layout/applistitem.xml index 941fe112a..5ccf7f6dc 100644 --- a/res/layout/applistitem.xml +++ b/res/layout/applistitem.xml @@ -44,6 +44,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentStart="true" + android:textAlignment="viewStart" android:layout_toStartOf="@id/status" /> From 955c2f5f6c13c0fdd082c9f055fd77e43cf874b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 23 Feb 2014 12:22:04 +0100 Subject: [PATCH 157/282] Bump build-tools to 19.0.2 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index aac83cc10..fded3e62b 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ subprojects { } apply plugin: 'android-library' android { - buildToolsVersion '19.0.1' + buildToolsVersion '19.0.2' packagingOptions { exclude "META-INF/LICENSE*" exclude "META-INF/NOTICE*" @@ -56,7 +56,7 @@ project(':extern:UniversalImageLoader:library') { android { compileSdkVersion 19 - buildToolsVersion '19.0.1' + buildToolsVersion '19.0.2' sourceSets { main { From 568224ba787b2d178434d8bbc137e7bbfa498148 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Sat, 15 Feb 2014 20:57:18 +1100 Subject: [PATCH 158/282] s/curVersion/upstreamVersion/g, added suggestedVersion. Refactored QueryBuilder. In order to support suggested version, I didn't want to have both suggested version + versionCode in the App table. Rather, just the code, and then use that (and the apps id) to join onto the apk table. This is something we wanted to do elsewhere, so I refactored the QueryBuilder class from the ApkProvider so that it can also be used by the AppProvider. --- TODO-before-release.md | 6 - src/org/fdroid/fdroid/AppDetails.java | 16 +- src/org/fdroid/fdroid/RepoXMLHandler.java | 6 +- src/org/fdroid/fdroid/UpdateService.java | 11 +- src/org/fdroid/fdroid/data/ApkProvider.java | 68 ++------ src/org/fdroid/fdroid/data/App.java | 41 +++-- src/org/fdroid/fdroid/data/AppProvider.java | 147 ++++++++++++------ src/org/fdroid/fdroid/data/DBHelper.java | 7 +- src/org/fdroid/fdroid/data/QueryBuilder.java | 105 +++++++++++++ .../fdroid/fdroid/views/AppListAdapter.java | 6 +- .../views/fragments/AppListFragment.java | 6 +- 11 files changed, 272 insertions(+), 147 deletions(-) create mode 100644 src/org/fdroid/fdroid/data/QueryBuilder.java diff --git a/TODO-before-release.md b/TODO-before-release.md index e635a2463..bcca63f4e 100644 --- a/TODO-before-release.md +++ b/TODO-before-release.md @@ -3,9 +3,3 @@ These issues are a must-fix before the next stable release: * Right after updating a repo, `Recently Updated` shows the apps correctly but the new apks don't show up on App Details until the whole app is restarted (or until the repos are wiped and re-downloaded) - -* `App.curVersion` is now used in some places where before we used - `App.curApk.version`, which means that e.g. app lists now show the current - version at upstream and not the latest stable version in the repository - (highly misleading to users, who might end up looking for versions not in - the repo yet) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 0cafa5e91..2e8f49867 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -128,7 +128,7 @@ public class AppDetails extends ListActivity { holder.version.setText(getString(R.string.version) + " " + apk.version - + (apk.vercode == app.curVercode ? " ☆" : "")); + + (apk.vercode == app.suggestedVercode ? " ☆" : "")); if (apk.vercode == app.getInstalledVerCode(getContext()) && mInstalledSigID != null && apk.sig != null @@ -534,7 +534,7 @@ public class AppDetails extends ListActivity { Apk curApk = null; for (int i = 0; i < adapter.getCount(); i ++) { Apk apk = adapter.getItem(i); - if (apk.vercode == app.curVercode) { + if (apk.vercode == app.suggestedVercode) { curApk = apk; break; } @@ -678,7 +678,7 @@ public class AppDetails extends ListActivity { } // Check count > 0 due to incompatible apps resulting in an empty list. - if (app.getInstalledVersion(this) == null && app.curVercode > 0 && + if (app.getInstalledVersion(this) == null && app.suggestedVercode > 0 && adapter.getCount() > 0) { MenuItemCompat.setShowAsAction(menu.add( Menu.NONE, INSTALL, 1, R.string.menu_install) @@ -716,7 +716,7 @@ public class AppDetails extends ListActivity { menu.add(Menu.NONE, IGNORETHIS, 2, R.string.menu_ignore_this) .setIcon(android.R.drawable.ic_menu_close_clear_cancel) .setCheckable(true) - .setChecked(app.ignoreThisUpdate >= app.curVercode); + .setChecked(app.ignoreThisUpdate >= app.suggestedVercode); } if (app.webURL.length() > 0) { menu.add(Menu.NONE, WEBSITE, 3, R.string.menu_website).setIcon( @@ -783,8 +783,8 @@ public class AppDetails extends ListActivity { case INSTALL: // Note that this handles updating as well as installing. - if (app.curVercode > 0) { - final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.curVercode); + if (app.suggestedVercode > 0) { + final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.suggestedVercode); install(apkToInstall); } return true; @@ -799,10 +799,10 @@ public class AppDetails extends ListActivity { return true; case IGNORETHIS: - if (app.ignoreThisUpdate >= app.curVercode) + if (app.ignoreThisUpdate >= app.suggestedVercode) app.ignoreThisUpdate = 0; else - app.ignoreThisUpdate = app.curVercode; + app.ignoreThisUpdate = app.suggestedVercode; item.setChecked(app.ignoreThisUpdate > 0); return true; diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java index a123c70ed..1c7fb5009 100644 --- a/src/org/fdroid/fdroid/RepoXMLHandler.java +++ b/src/org/fdroid/fdroid/RepoXMLHandler.java @@ -226,12 +226,12 @@ public class RepoXMLHandler extends DefaultHandler { curapp.lastUpdated = null; } } else if (curel.equals("marketversion")) { - curapp.curVersion = str; + curapp.upstreamVersion = str; } else if (curel.equals("marketvercode")) { try { - curapp.curVercode = Integer.parseInt(str); + curapp.upstreamVercode = Integer.parseInt(str); } catch (NumberFormatException ex) { - curapp.curVercode = -1; + curapp.upstreamVercode = -1; } } else if (curel.equals("categories")) { curapp.categories = Utils.CommaSeparatedList.make(str); diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index dc3aa0711..bd53a2f36 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -400,19 +400,19 @@ public class UpdateService extends IntentService implements ProgressListener { private static void calcCurrentApkForApp(App app, List apksForApp) { Apk latestApk = null; // Try and return the real current version first. It will find the - // closest version smaller than the curVercode, being the same + // closest version smaller than the upstreamVercode, being the same // vercode if it exists. - if (app.curVercode > 0) { + if (app.upstreamVercode > 0) { int latestcode = -1; for (Apk apk : apksForApp) { if ((!app.compatible || apk.compatible) - && apk.vercode <= app.curVercode + && apk.vercode <= app.upstreamVercode && apk.vercode > latestcode) { latestApk = apk; latestcode = apk.vercode; } } - } else if (app.curVercode == -1) { + } else if (app.upstreamVercode == -1) { // If the current version was not set we return the most recent apk. int latestCode = -1; for (Apk apk : apksForApp) { @@ -425,8 +425,7 @@ public class UpdateService extends IntentService implements ProgressListener { } if (latestApk != null) { - app.curVercode = latestApk.vercode; - app.curVersion = latestApk.version; + app.suggestedVercode = latestApk.vercode; } } diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java index 0d070653f..8216c3c86 100644 --- a/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -221,15 +221,16 @@ public class ApkProvider extends FDroidProvider { return matcher; } - private static class QueryBuilder { - - private StringBuilder fields = new StringBuilder(); - private StringBuilder tables = new StringBuilder(DBHelper.TABLE_APK + " AS apk"); - private String selection = null; - private String orderBy = null; + private static class Query extends QueryBuilder { private boolean repoTableRequired = false; + @Override + protected String getRequiredTables() { + return DBHelper.TABLE_APK + " AS apk"; + } + + @Override public void addField(String field) { if (REPO_FIELDS.containsKey(field)) { addRepoField(REPO_FIELDS.get(field), field); @@ -242,63 +243,14 @@ public class ApkProvider extends FDroidProvider { } } - public void addRepoField(String field, String alias) { + private void addRepoField(String field, String alias) { if (!repoTableRequired) { repoTableRequired = true; - tables.append(" LEFT JOIN "); - tables.append(DBHelper.TABLE_REPO); - tables.append(" AS repo ON (apk.repo = repo._id) "); + leftJoin(DBHelper.TABLE_REPO, "repo", "apk.repo = repo._id"); } appendField(field, "repo", alias); } - private void appendField(String field) { - appendField(field, null, null); - } - - private void appendField(String field, String tableAlias) { - appendField(field, tableAlias, null); - } - - private void appendField(String field, String tableAlias, - String fieldAlias) { - if (fields.length() != 0) { - fields.append(','); - } - - if (tableAlias != null) { - fields.append(tableAlias).append('.'); - } - - fields.append(field); - - if (fieldAlias != null) { - fields.append(" AS ").append(fieldAlias); - } - } - - public void addSelection(String selection) { - this.selection = selection; - } - - public void addOrderBy(String orderBy) { - this.orderBy = orderBy; - } - - @Override - public String toString() { - - StringBuilder suffix = new StringBuilder(); - if (selection != null) { - suffix.append(" WHERE ").append(selection); - } - - if (orderBy != null) { - suffix.append(" ORDER BY ").append(orderBy); - } - - return "SELECT " + fields + " FROM " + tables + suffix; - } } private QuerySelection queryApp(String appId) { @@ -377,7 +329,7 @@ public class ApkProvider extends FDroidProvider { throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri); } - QueryBuilder queryBuilder = new QueryBuilder(); + Query queryBuilder = new Query(); for (String field : projection) { queryBuilder.addField(field); } diff --git a/src/org/fdroid/fdroid/data/App.java b/src/org/fdroid/fdroid/data/App.java index cec9b452f..30688b21a 100644 --- a/src/org/fdroid/fdroid/data/App.java +++ b/src/org/fdroid/fdroid/data/App.java @@ -41,8 +41,18 @@ public class App extends ValueObject implements Comparable { public String flattrID; - public String curVersion; - public int curVercode; + public String upstreamVersion; + public int upstreamVercode; + + /** + * Unlike other public fields, this is only accessible via a getter, to + * emphasise that setting it wont do anything. In order to change this, + * you need to change suggestedVercode to an apk which is in the apk table. + */ + private String suggestedVersion; + + public int suggestedVercode; + public Date added; public Date lastUpdated; @@ -114,10 +124,14 @@ public class App extends ValueObject implements Comparable { dogecoinAddr = cursor.getString(i); } else if (column.equals(AppProvider.DataColumns.FLATTR_ID)) { flattrID = cursor.getString(i); - } else if (column.equals(AppProvider.DataColumns.CURRENT_VERSION)) { - curVersion = cursor.getString(i); - } else if (column.equals(AppProvider.DataColumns.CURRENT_VERSION_CODE)) { - curVercode = cursor.getInt(i); + } else if (column.equals(AppProvider.DataColumns.SuggestedApk.VERSION)) { + suggestedVersion = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.SUGGESTED_VERSION_CODE)) { + suggestedVercode = cursor.getInt(i); + } else if (column.equals(AppProvider.DataColumns.UPSTREAM_VERSION_CODE)) { + upstreamVercode = cursor.getInt(i); + } else if (column.equals(AppProvider.DataColumns.UPSTREAM_VERSION)) { + upstreamVersion = cursor.getString(i); } else if (column.equals(AppProvider.DataColumns.ADDED)) { added = ValueObject.toDate(cursor.getString(i)); } else if (column.equals(AppProvider.DataColumns.LAST_UPDATED)) { @@ -158,8 +172,9 @@ public class App extends ValueObject implements Comparable { values.put(AppProvider.DataColumns.FLATTR_ID, flattrID); values.put(AppProvider.DataColumns.ADDED, added == null ? "" : Utils.DATE_FORMAT.format(added)); values.put(AppProvider.DataColumns.LAST_UPDATED, added == null ? "" : Utils.DATE_FORMAT.format(lastUpdated)); - values.put(AppProvider.DataColumns.CURRENT_VERSION, curVersion); - values.put(AppProvider.DataColumns.CURRENT_VERSION_CODE, curVercode); + values.put(AppProvider.DataColumns.SUGGESTED_VERSION_CODE, suggestedVercode); + values.put(AppProvider.DataColumns.UPSTREAM_VERSION, upstreamVersion); + values.put(AppProvider.DataColumns.UPSTREAM_VERSION_CODE, upstreamVercode); values.put(AppProvider.DataColumns.CATEGORIES, Utils.CommaSeparatedList.str(categories)); values.put(AppProvider.DataColumns.ANTI_FEATURES, Utils.CommaSeparatedList.str(antiFeatures)); values.put(AppProvider.DataColumns.REQUIREMENTS, Utils.CommaSeparatedList.str(requirements)); @@ -207,9 +222,9 @@ public class App extends ValueObject implements Comparable { */ public boolean hasUpdates(Context context) { boolean updates = false; - if (curVercode > 0) { + if (suggestedVercode > 0) { int installedVerCode = getInstalledVerCode(context); - updates = (installedVerCode > 0 && installedVerCode < curVercode); + updates = (installedVerCode > 0 && installedVerCode < suggestedVercode); } return updates; } @@ -218,7 +233,7 @@ public class App extends ValueObject implements Comparable { // to be notified about them public boolean canAndWantToUpdate(Context context) { boolean canUpdate = hasUpdates(context); - boolean wantsUpdate = !ignoreAllUpdates && ignoreThisUpdate < curVercode; + boolean wantsUpdate = !ignoreAllUpdates && ignoreThisUpdate < suggestedVercode; return canUpdate && wantsUpdate && !isFiltered(); } @@ -227,4 +242,8 @@ public class App extends ValueObject implements Comparable { public boolean isFiltered() { return new AppFilter().filter(this); } + + public String getSuggestedVersion() { + return suggestedVersion; + } } diff --git a/src/org/fdroid/fdroid/data/AppProvider.java b/src/org/fdroid/fdroid/data/AppProvider.java index f404df0c0..9475cfb62 100644 --- a/src/org/fdroid/fdroid/data/AppProvider.java +++ b/src/org/fdroid/fdroid/data/AppProvider.java @@ -60,7 +60,7 @@ public class AppProvider extends FDroidProvider { public static List categories(Context context) { ContentResolver resolver = context.getContentResolver(); Uri uri = getContentUri(); - String[] projection = { "DISTINCT " + DataColumns.CATEGORIES }; + String[] projection = { DataColumns.CATEGORIES }; Cursor cursor = resolver.query(uri, projection, null, null, null ); Set categorySet = new HashSet(); if (cursor != null) { @@ -110,7 +110,7 @@ public class AppProvider extends FDroidProvider { public interface DataColumns { - public static final String _ID = "rowid as _id"; + public static final String _ID = "rowid as _id"; // Required for CursorLoaders public static final String _COUNT = "_count"; public static final String IS_COMPATIBLE = "compatible"; public static final String APP_ID = "id"; @@ -127,8 +127,9 @@ public class AppProvider extends FDroidProvider { public static final String LITECOIN_ADDR = "litecoinAddr"; public static final String DOGECOIN_ADDR = "dogecoinAddr"; public static final String FLATTR_ID = "flattrID"; - public static final String CURRENT_VERSION = "curVersion"; - public static final String CURRENT_VERSION_CODE = "curVercode"; + public static final String SUGGESTED_VERSION_CODE = "suggestedVercode"; + public static final String UPSTREAM_VERSION = "upstreamVersion"; + public static final String UPSTREAM_VERSION_CODE = "upstreamVercode"; public static final String CURRENT_APK = null; public static final String ADDED = "added"; public static final String LAST_UPDATED = "lastUpdated"; @@ -147,16 +148,73 @@ public class AppProvider extends FDroidProvider { public static final String UPDATED = null; public static final String APKS = null; + public interface SuggestedApk { + public static final String VERSION = "suggestedApkVersion"; + } + public static String[] ALL = { IS_COMPATIBLE, APP_ID, NAME, SUMMARY, ICON, DESCRIPTION, LICENSE, WEB_URL, TRACKER_URL, SOURCE_URL, DONATE_URL, BITCOIN_ADDR, LITECOIN_ADDR, DOGECOIN_ADDR, FLATTR_ID, - CURRENT_VERSION, CURRENT_VERSION_CODE, ADDED, LAST_UPDATED, + UPSTREAM_VERSION, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED, CATEGORIES, ANTI_FEATURES, REQUIREMENTS, IGNORE_ALLUPDATES, - IGNORE_THISUPDATE, ICON_URL + IGNORE_THISUPDATE, ICON_URL, SUGGESTED_VERSION_CODE, + SuggestedApk.VERSION }; } + private static class Query extends QueryBuilder { + + private boolean isSuggestedApkTableAdded = false; + + private boolean categoryFieldAdded = false; + + @Override + protected String getRequiredTables() { + return DBHelper.TABLE_APP; + } + + @Override + protected boolean isDistinct() { + return fieldCount() == 1 && categoryFieldAdded; + } + + @Override + public void addField(String field) { + if (field.equals(DataColumns.SuggestedApk.VERSION)) { + addSuggestedApkVersionField(); + } else if (field.equals(DataColumns._COUNT)) { + appendCountField(); + } else { + if (field.equals(DataColumns.CATEGORIES)) { + categoryFieldAdded = true; + } + appendField(field, "fdroid_app"); + } + } + + private void appendCountField() { + appendField("COUNT(*) AS " + DataColumns._COUNT); + } + + private void addSuggestedApkVersionField() { + addSuggestedApkField( + ApkProvider.DataColumns.VERSION, + DataColumns.SuggestedApk.VERSION); + } + + private void addSuggestedApkField(String fieldName, String alias) { + if (!isSuggestedApkTableAdded) { + isSuggestedApkTableAdded = true; + leftJoin( + DBHelper.TABLE_APK, + "suggestedApk", + "fdroid_app.suggestedVercode = suggestedApk.vercode AND fdroid_app.id = suggestedApk.id"); + } + appendField(fieldName, "suggestedApk", alias); + } + } + private static final String PROVIDER_NAME = "AppProvider"; private static final UriMatcher matcher = new UriMatcher(-1); @@ -274,18 +332,18 @@ public class AppProvider extends FDroidProvider { private QuerySelection queryCanUpdate() { Map installedApps = Utils.getInstalledApps(getContext()); - String ignoreCurrent = " ignoreThisUpdate != curVercode "; - String ignoreAll = " ignoreAllUpdates != 1 "; + String ignoreCurrent = " fdroid_app.ignoreThisUpdate != fdroid_app.suggestedVercode "; + String ignoreAll = " fdroid_app.ignoreAllUpdates != 1 "; String ignore = " ( " + ignoreCurrent + " AND " + ignoreAll + " ) "; StringBuilder where = new StringBuilder( ignore + " AND ( 0 "); String[] selectionArgs = new String[installedApps.size() * 2]; int i = 0; for (PackageInfo info : installedApps.values() ) { - where.append(" OR ( ") - .append(AppProvider.DataColumns.APP_ID) - .append(" = ? AND ") - .append(DataColumns.CURRENT_VERSION_CODE) + where.append(" OR ( fdroid_app.") + .append(DataColumns.APP_ID) + .append(" = ? AND fdroid_app.") + .append(DataColumns.SUGGESTED_VERSION_CODE) .append(" > ?) "); selectionArgs[ i * 2 ] = info.packageName; selectionArgs[ i * 2 + 1 ] = Integer.toString(info.versionCode); @@ -302,7 +360,7 @@ public class AppProvider extends FDroidProvider { String[] selectionArgs = new String[installedApps.size()]; int i = 0; for (Map.Entry entry : installedApps.entrySet() ) { - where.append(" OR ") + where.append(" OR fdroid_app.") .append(AppProvider.DataColumns.APP_ID) .append(" = ? "); selectionArgs[i] = entry.getKey(); @@ -316,27 +374,29 @@ public class AppProvider extends FDroidProvider { private QuerySelection querySearch(String keywords) { keywords = "%" + keywords + "%"; String selection = - "id like ? OR " + - "name like ? OR " + - "summary like ? OR " + - "description like ? "; + "fdroid_app.id like ? OR " + + "fdroid_app.name like ? OR " + + "fdroid_app.summary like ? OR " + + "fdroid_app.description like ? "; String[] args = new String[] { keywords, keywords, keywords, keywords}; return new QuerySelection(selection, args); } + private QuerySelection querySingle(String id) { + String selection = "fdroid_app.id = ?"; + String[] args = { id }; + return new QuerySelection(selection, args); + } + private QuerySelection queryNewlyAdded() { - String selection = "added > ?"; - String[] args = new String[] { - Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory()) - }; + String selection = "fdroid_app.added > ?"; + String[] args = { Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory()) }; return new QuerySelection(selection, args); } private QuerySelection queryRecentlyUpdated() { - String selection = "added != lastUpdated AND lastUpdated > ?"; - String[] args = new String[] { - Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory()) - }; + String selection = "fdroid_app.added != fdroid_app.lastUpdated AND fdroid_app.lastUpdated > ?"; + String[] args = { Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory()) }; return new QuerySelection(selection, args); } @@ -344,11 +404,11 @@ public class AppProvider extends FDroidProvider { // TODO: In the future, add a new table for categories, // so we can join onto it. String selection = - " categories = ? OR " + // Only category e.g. "internet" - " categories LIKE ? OR " + // First category e.g. "internet,%" - " categories LIKE ? OR " + // Last category e.g. "%,internet" - " categories LIKE ? "; // One of many categories e.g. "%,internet,%" - String[] args = new String[] { + " fdroid_app.categories = ? OR " + // Only category e.g. "internet" + " fdroid_app.categories LIKE ? OR " + // First category e.g. "internet,%" + " fdroid_app.categories LIKE ? OR " + // Last category e.g. "%,internet" + " fdroid_app.categories LIKE ? "; // One of many categories e.g. "%,internet,%" + String[] args = { category, category + ",%", "%," + category, @@ -364,7 +424,7 @@ public class AppProvider extends FDroidProvider { private QuerySelection queryApps(String appIds) { String[] args = appIds.split(","); - String selection = "id IN (" + generateQuestionMarksForInClause(args.length) + ")"; + String selection = "fdroid_app.id IN (" + generateQuestionMarksForInClause(args.length) + ")"; return new QuerySelection(selection, args); } @@ -376,9 +436,7 @@ public class AppProvider extends FDroidProvider { break; case CODE_SINGLE: - query = query.add( - DataColumns.APP_ID + " = ?", - new String[] { uri.getLastPathSegment() } ); + query = query.add(querySingle(uri.getLastPathSegment())); break; case CAN_UPDATE: @@ -406,12 +464,12 @@ public class AppProvider extends FDroidProvider { break; case RECENTLY_UPDATED: - sortOrder = DataColumns.LAST_UPDATED + " DESC"; + sortOrder = " fdroid_app.lastUpdated DESC"; query = query.add(queryRecentlyUpdated()); break; case NEWLY_ADDED: - sortOrder = DataColumns.ADDED + " DESC"; + sortOrder = " fdroid_app.added DESC"; query = query.add(queryNewlyAdded()); break; @@ -421,18 +479,15 @@ public class AppProvider extends FDroidProvider { } if (AppProvider.DataColumns.NAME.equals(sortOrder)) { - sortOrder = " lower( " + sortOrder + " ) "; + sortOrder = " lower( fdroid_app." + sortOrder + " ) "; } - for (String field : projection) { - if (field.equals(DataColumns._COUNT)) { - projection = new String[] { "COUNT(*) AS " + DataColumns._COUNT }; - break; - } - } + Query q = new Query(); + q.addFields(projection); + q.addSelection(query.getSelection()); + q.addOrderBy(sortOrder); - Cursor cursor = read().query(getTableName(), projection, query.getSelection(), - query.getArgs(), null, null, sortOrder); + Cursor cursor = read().rawQuery(q.toString(), query.getArgs()); cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; } @@ -472,7 +527,7 @@ public class AppProvider extends FDroidProvider { switch (matcher.match(uri)) { case CODE_SINGLE: - query = query.add(new QuerySelection("id = ?", new String[] { uri.getLastPathSegment()})); + query = query.add(querySingle(uri.getLastPathSegment())); break; default: diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index 2a531362f..43a435efd 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -67,8 +67,9 @@ public class DBHelper extends SQLiteOpenHelper { + "webURL text, " + "trackerURL text, " + "sourceURL text, " - + "curVersion text," - + "curVercode integer," + + "suggestedVercode text," + + "upstreamVersion text," + + "upstreamVercode integer," + "antiFeatures string," + "donateURL string," + "bitcoinAddr string," @@ -85,7 +86,7 @@ public class DBHelper extends SQLiteOpenHelper { + "iconUrl text, " + "primary key(id));"; - private static final int DB_VERSION = 40; + private static final int DB_VERSION = 41; private Context context; diff --git a/src/org/fdroid/fdroid/data/QueryBuilder.java b/src/org/fdroid/fdroid/data/QueryBuilder.java new file mode 100644 index 000000000..6c988efc0 --- /dev/null +++ b/src/org/fdroid/fdroid/data/QueryBuilder.java @@ -0,0 +1,105 @@ +package org.fdroid.fdroid.data; + +import java.util.ArrayList; +import java.util.List; + +abstract class QueryBuilder { + + private List fields = new ArrayList(); + private StringBuilder tables = new StringBuilder(getRequiredTables()); + private String selection = null; + private String orderBy = null; + + protected abstract String getRequiredTables(); + + public abstract void addField(String field); + + protected int fieldCount() { + return fields.size(); + } + + public void addFields(String[] fields) { + for (String field : fields) { + addField(field); + } + } + + protected boolean isDistinct() { + return false; + } + + protected void appendField(String field) { + appendField(field, null, null); + } + + protected void appendField(String field, String tableAlias) { + appendField(field, tableAlias, null); + } + + protected final void appendField(String field, String tableAlias, + String fieldAlias) { + + StringBuilder fieldBuilder = new StringBuilder(); + + if (tableAlias != null) { + fieldBuilder.append(tableAlias).append('.'); + } + + fieldBuilder.append(field); + + if (fieldAlias != null) { + fieldBuilder.append(" AS ").append(fieldAlias); + } + + fields.add(fieldBuilder.toString()); + } + + public void addSelection(String selection) { + this.selection = selection; + } + + public void addOrderBy(String orderBy) { + this.orderBy = orderBy; + } + + protected final void leftJoin(String table, String alias, + String condition) { + tables.append(" LEFT JOIN "); + tables.append(table); + if (alias != null) { + tables.append(" AS "); + tables.append(alias); + } + tables.append(" ON ("); + tables.append(condition); + tables.append(")"); + } + + private String fieldsSql() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < fields.size(); i ++) { + if (i > 0) { + sb.append(','); + } + sb.append(fields.get(i)); + } + return sb.toString(); + } + + private String whereSql() { + return selection != null ? " WHERE " + selection : ""; + } + + private String orderBySql() { + return orderBy != null ? " ORDER BY " + orderBy : ""; + } + + private String tablesSql() { + return tables.toString(); + } + + public String toString() { + String distinct = isDistinct() ? " DISTINCT " : ""; + return "SELECT " + distinct + fieldsSql() + " FROM " + tablesSql() + whereSql() + orderBySql(); + } +} diff --git a/src/org/fdroid/fdroid/views/AppListAdapter.java b/src/org/fdroid/fdroid/views/AppListAdapter.java index 5659283fd..ca0336d87 100644 --- a/src/org/fdroid/fdroid/views/AppListAdapter.java +++ b/src/org/fdroid/fdroid/views/AppListAdapter.java @@ -130,14 +130,14 @@ abstract public class AppListAdapter extends CursorAdapter { private String getVersionInfo(App app) { - if (app.curVercode <= 0) { + if (app.suggestedVercode <= 0) { return null; } PackageInfo installedInfo = app.getInstalledInfo(mContext); if (installedInfo == null) { - return ellipsize(app.curVersion, 12); + return ellipsize(app.getSuggestedVersion(), 12); } String installedVersionString = installedInfo.versionName; @@ -145,7 +145,7 @@ abstract public class AppListAdapter extends CursorAdapter { if (app.canAndWantToUpdate(mContext) && showStatusUpdate()) { return ellipsize(installedVersionString, 8) + - " → " + ellipsize(app.curVersion, 8); + " → " + ellipsize(app.getSuggestedVersion(), 8); } if (installedVersionCode > 0 && showStatusInstalled()) { diff --git a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java index 3ad2c08a3..b3f2a91a4 100644 --- a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java @@ -27,7 +27,7 @@ abstract public class AppListFragment extends ListFragment implements LoaderManager.LoaderCallbacks { public static final String[] APP_PROJECTION = { - AppProvider.DataColumns._ID, + AppProvider.DataColumns._ID, // Required for cursor loader to work. AppProvider.DataColumns.APP_ID, AppProvider.DataColumns.NAME, AppProvider.DataColumns.SUMMARY, @@ -35,8 +35,8 @@ abstract public class AppListFragment extends ListFragment implements AppProvider.DataColumns.LICENSE, AppProvider.DataColumns.ICON, AppProvider.DataColumns.ICON_URL, - AppProvider.DataColumns.CURRENT_VERSION, - AppProvider.DataColumns.CURRENT_VERSION_CODE, + AppProvider.DataColumns.SuggestedApk.VERSION, + AppProvider.DataColumns.SUGGESTED_VERSION_CODE, AppProvider.DataColumns.REQUIREMENTS, // Needed for filtering apps that require root. }; From 87f2da7e2ff4372d2008e4c3185a7a58a32457a6 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Sun, 23 Feb 2014 09:19:23 +1100 Subject: [PATCH 159/282] Fix 'Number of apps' sql exception in repo details. --- src/org/fdroid/fdroid/data/ApkProvider.java | 4 ++++ src/org/fdroid/fdroid/data/RepoProvider.java | 9 ++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java index 8216c3c86..e607a53f4 100644 --- a/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -100,6 +100,8 @@ public class ApkProvider extends FDroidProvider { public interface DataColumns extends BaseColumns { + public static String _COUNT_DISTINCT_ID = "countDistinct"; + public static String APK_ID = "id"; public static String VERSION = "version"; public static String REPO_ID = "repo"; @@ -238,6 +240,8 @@ public class ApkProvider extends FDroidProvider { appendField("rowid", "apk", "_id"); } else if (field.equals(DataColumns._COUNT)) { appendField("COUNT(*) AS " + DataColumns._COUNT); + } else if (field.equals(DataColumns._COUNT_DISTINCT_ID)) { + appendField("COUNT(DISTINCT apk.id) AS " + DataColumns._COUNT_DISTINCT_ID); } else { appendField(field, "apk"); } diff --git a/src/org/fdroid/fdroid/data/RepoProvider.java b/src/org/fdroid/fdroid/data/RepoProvider.java index 530ce8ecf..77e567a4d 100644 --- a/src/org/fdroid/fdroid/data/RepoProvider.java +++ b/src/org/fdroid/fdroid/data/RepoProvider.java @@ -174,11 +174,9 @@ public class RepoProvider extends FDroidProvider { public static int countAppsForRepo(Context context, long repoId) { ContentResolver resolver = context.getContentResolver(); - String[] projection = { "COUNT(distinct id)" }; - String selection = "repo = ?"; - String[] args = { Long.toString(repoId) }; - Uri apkUri = ApkProvider.getContentUri(); - Cursor result = resolver.query(apkUri, projection, selection, args, null); + String[] projection = { ApkProvider.DataColumns._COUNT_DISTINCT_ID }; + Uri apkUri = ApkProvider.getRepoUri(repoId); + Cursor result = resolver.query(apkUri, projection, null, null, null); if (result != null && result.getCount() > 0) { result.moveToFirst(); return result.getInt(0); @@ -189,6 +187,7 @@ public class RepoProvider extends FDroidProvider { } public interface DataColumns extends BaseColumns { + public static String ADDRESS = "address"; public static String NAME = "name"; public static String DESCRIPTION = "description"; From 45d046b445a41e5d75e25dea3d6a4c05eac59da1 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Mon, 24 Feb 2014 11:01:56 +1100 Subject: [PATCH 160/282] Fix unique key violation in update service. When two repos both add an apk with same version and id, then it would break. --- src/org/fdroid/fdroid/UpdateService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index bd53a2f36..dbc2ffdcb 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -628,6 +628,7 @@ public class UpdateService extends IntentService implements ProgressListener { operations.add(updateExistingApk(apk)); } else { operations.add(insertNewApk(apk)); + knownApks.add(apk); // In case another repo has the same version/id combo for this apk. } } From 7111f54c9b6658ee4a5f444f93c49b5df861caa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 27 Feb 2014 11:32:20 +0100 Subject: [PATCH 161/282] Update submodules --- extern/UniversalImageLoader | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/UniversalImageLoader b/extern/UniversalImageLoader index 2de3eb620..aa1cde8c2 160000 --- a/extern/UniversalImageLoader +++ b/extern/UniversalImageLoader @@ -1 +1 @@ -Subproject commit 2de3eb620e4ca4b57cfd1593f70f88cfa48a9584 +Subproject commit aa1cde8c20f6ef57de994ce8f72f772a14800706 From 4004b6251f4990fbf74905cdcf748d6452ac91c4 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 27 Feb 2014 22:56:16 +1100 Subject: [PATCH 162/282] Fixed issue 474 - crash on "Up" button from ManageRepos. Not sure that the "parent" activity of ManageRepos is required in the manifest any more, due to the fact that the main use seems to be to direct the "NavUtils.navigateUpSameTask" method uses it, but this change switches to "NavUtils.navigateUpTo" and specifies the activity explicitly. --- src/org/fdroid/fdroid/ManageRepo.java | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/org/fdroid/fdroid/ManageRepo.java b/src/org/fdroid/fdroid/ManageRepo.java index 757def9db..102a8faa3 100644 --- a/src/org/fdroid/fdroid/ManageRepo.java +++ b/src/org/fdroid/fdroid/ManageRepo.java @@ -62,19 +62,30 @@ public class ManageRepo extends FragmentActivity { @Override public void finish() { Intent ret = new Intent(); - if (listFragment != null && listFragment.hasChanged()) { - Log.i("FDroid", "Repo details have changed, prompting for update."); - ret.putExtra(REQUEST_UPDATE, true); - } + markChangedIfRequired(ret); setResult(Activity.RESULT_OK, ret); super.finish(); } + private boolean hasChanged() { + return listFragment != null && listFragment.hasChanged(); + } + + private void markChangedIfRequired(Intent intent) { + if (hasChanged()) { + Log.i("FDroid", "Repo details have changed, prompting for update."); + intent.putExtra(REQUEST_UPDATE, true); + } + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); + Intent destIntent = new Intent(this, FDroid.class); + markChangedIfRequired(destIntent); + setResult(RESULT_OK, destIntent); + NavUtils.navigateUpTo(this, destIntent); return true; } return super.onOptionsItemSelected(item); From aa9bfefd55314193f36a2eb22d97ae28e9f85a3b Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 28 Feb 2014 10:32:32 +1100 Subject: [PATCH 163/282] Fixed bug with only one apk being added from index for each app. --- src/org/fdroid/fdroid/UpdateService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index dbc2ffdcb..5ff446905 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -618,7 +618,7 @@ public class UpdateService extends IntentService implements ProgressListener { for (Apk apk : apksToUpdate) { boolean known = false; for (Apk knownApk : knownApks) { - if (knownApk.id.equals(apk.id) && knownApk.version.equals(knownApk.version)) { + if (knownApk.id.equals(apk.id) && knownApk.vercode == apk.vercode) { known = true; break; } From d647bfb09712d8e7cf0df4bdab9751f36ee4898a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 28 Feb 2014 00:37:34 +0100 Subject: [PATCH 164/282] Release 0.60-test --- AndroidManifest.xml | 4 ++-- res/values/no_trans.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index fb848a3aa..0c47514ba 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="600" + android:versionName="0.60-test" > F-Droid - 0.59-test + 0.60-test https://f-droid.org team@f-droid.org From bf040a73fa6f98155648158b0244e60db862d74b Mon Sep 17 00:00:00 2001 From: F-Droid Translatebot Date: Wed, 5 Mar 2014 21:44:48 +0000 Subject: [PATCH 165/282] Translation updates --- res/values-ar/array.xml | 5 +++ res/values-bg/array.xml | 5 +++ res/values-bg/strings.xml | 3 +- res/values-ca/array.xml | 5 +++ res/values-ca/strings.xml | 7 ++-- res/values-de/array.xml | 5 +++ res/values-de/strings.xml | 69 ++++++++++++++++++++++++++++--- res/values-el/array.xml | 5 +++ res/values-el/strings.xml | 7 ++-- res/values-eo/array.xml | 5 +++ res/values-es/array.xml | 5 +++ res/values-es/strings.xml | 65 ++++++++++++++++++++++++++++- res/values-eu/array.xml | 5 +++ res/values-eu/strings.xml | 5 ++- res/values-fa/array.xml | 5 +++ res/values-fa/strings.xml | 5 ++- res/values-fi/array.xml | 5 +++ res/values-fi/strings.xml | 5 ++- res/values-fr/array.xml | 5 +++ res/values-fr/strings.xml | 5 ++- res/values-gl/array.xml | 5 +++ res/values-gl/strings.xml | 3 +- res/values-gu/array.xml | 5 +++ res/values-it/array.xml | 5 +++ res/values-it/strings.xml | 62 ++++++++++++++++++++++++++-- res/values-ko/array.xml | 5 +++ res/values-ko/strings.xml | 3 +- res/values-nb/array.xml | 5 +++ res/values-nb/strings.xml | 5 ++- res/values-nl/array.xml | 5 +++ res/values-nl/strings.xml | 29 +++++++++---- res/values-pl/array.xml | 5 +++ res/values-pl/strings.xml | 74 ++++++++++++++++++++++++++++++--- res/values-pt-rBR/array.xml | 5 +++ res/values-pt-rBR/strings.xml | 7 ++-- res/values-qqq/strings.xml | 5 +++ res/values-ro/array.xml | 5 +++ res/values-ro/strings.xml | 10 ++--- res/values-ru/array.xml | 5 +++ res/values-ru/strings.xml | 78 +++++++++++++++++++++++++++++++++-- res/values-sl/array.xml | 5 +++ res/values-sl/strings.xml | 3 +- res/values-sr/array.xml | 5 +++ res/values-sr/strings.xml | 7 ++-- res/values-sv/array.xml | 5 +++ res/values-sv/strings.xml | 42 ++++++++++++++++++- res/values-tr/array.xml | 5 +++ res/values-tr/strings.xml | 3 +- res/values-ug/array.xml | 5 +++ res/values-ug/strings.xml | 3 +- res/values-uk/array.xml | 5 +++ res/values-uk/strings.xml | 3 +- res/values-zh-rCN/array.xml | 5 +++ res/values-zh-rCN/strings.xml | 1 + 54 files changed, 584 insertions(+), 65 deletions(-) create mode 100644 res/values-qqq/strings.xml diff --git a/res/values-ar/array.xml b/res/values-ar/array.xml index f26ac258c..6b1fbe25d 100644 --- a/res/values-ar/array.xml +++ b/res/values-ar/array.xml @@ -11,4 +11,9 @@ غامق فاتح + + معطل (غير آمن) + عادي + مكتمل + diff --git a/res/values-bg/array.xml b/res/values-bg/array.xml index e81f9ad28..d7738bcf9 100644 --- a/res/values-bg/array.xml +++ b/res/values-bg/array.xml @@ -11,4 +11,9 @@ Dark Light + + Изключено (опасно) + Нормално + Пълно + diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml index de283c920..8ae9aa460 100644 --- a/res/values-bg/strings.xml +++ b/res/values-bg/strings.xml @@ -46,7 +46,7 @@ %d налични актуализации. Актуализации на F-Droid са налични Моля изчакай - Обновявани на списъка с приложения… + Обновявани на списъка с приложения... Взимане на приложението от Адрес на хранилището Списъкът на хранилищата е променен. @@ -73,6 +73,7 @@ Дисплей Експерт Търсене на приложения + Вид на синхронизация на базата данни Съвместимост на приложенията Root достъп Игнорирай сензорния екран diff --git a/res/values-ca/array.xml b/res/values-ca/array.xml index 01d48006c..4ea57069f 100644 --- a/res/values-ca/array.xml +++ b/res/values-ca/array.xml @@ -11,4 +11,9 @@ Fosc Clar + + Desactivat (no segur) + Normal + Complet + diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index cccc8a333..ad655582c 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -29,7 +29,7 @@ Publicat amb la llicència GNU GPL v3. Un dipòsit és una font d\'aplicacions. Per afegir-ne un, premeu ara el botó MENÚ i entreu la seva URL. -L\'adreça d\'un dipòsit té un aspecte com ara: https://f-droid.org/repo +L\'adreça d\'un dipòsit té un aspecte com ara: http://f-droid.org/repo Instal·lat No està instal·lat S\'ha afegit a %s @@ -48,7 +48,7 @@ L\'adreça d\'un dipòsit té un aspecte com ara: https://f-droid.org/repoHi ha %d actualitzacions disponibles. Hi ha actualitzacions de l\'F-Droid disponibles Un moment si us plau - S\'està actualitzant la llista d\'aplicacions… + S\'està actualitzant la llista d\'aplicacions... S\'està obtenint l\'aplicació des de Adreça del dipòsit Aquest repositori ja existeix. @@ -84,6 +84,7 @@ La voleu actualitzar? Pantalla Usuari expert Cerca aplicacions + Mode de sincronització de la base de dades Compatibilitat de les aplicacions Versions incompatibles Root @@ -99,7 +100,7 @@ La voleu actualitzar? %1$s S\'està connectant a %1$s - S\'està comprovant la compatibilitat de les aplicacions amb el vostre dispositiu… + S\'està comprovant la compatibilitat de les aplicacions amb el vostre dispositiu... No es fa servir cap permís. Permisos de la versió %s Mostra els permisos diff --git a/res/values-de/array.xml b/res/values-de/array.xml index 722843fd8..cd7204197 100644 --- a/res/values-de/array.xml +++ b/res/values-de/array.xml @@ -11,4 +11,9 @@ Dunkel Hell + + Aus (unsicher) + Normal + Vollständig + diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 4b17427bf..6dc755036 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -7,14 +7,25 @@ Es sieht so aus, als sei dieses Paket mit Ihrem Gerät nicht kompatibel. Möchten Sie trotzdem versuchen es zu installieren? Sie versuchen eine vorherige Version einer bereits installierten Anwendung zu installieren. Dies kann zu Fehlverhalten der Anwendung und gegebenenfalls zu Datenverlust führen. Möchten Sie dennoch fortfahren? Version - Heruntergeladene Anwendungen zwischenspeichern + Bearbeiten + Löschen + Anwendungszwischenspeicher + Heruntergeladene Programmpakete auf der SD-Karte behalten + Installationsdateien nicht behalten Aktualisierungen Andere Letzte Aktualisierung der Paketquellen: %s niemals + Automatischer Aktualisierungsintervall + Keine automatischen Aktualisierungen der Anwendungsliste Nur über WLAN + Anwendungsliste nur über WLAN automatisch aktualisieren + Anwendungsliste immer automatisch aktualisieren Benachrichtigen + Benachrichtigen, wenn Aktualisierungen verfügbar sind + Über Aktualisierungen nicht benachrichtigen Aktualisierungsverlauf + Anzahl in Tagen, an denen neue oder kürzlich geänderte Anwendungen angezeigt werden: %s Suchergebnisse Anwendungsdetails Keine passende Anwendung gefunden @@ -30,7 +41,7 @@ Veröffentlicht unter der GNU GPLv3 Lizenz. Eine Paketquelle ist eine Sammlung von Anwendungen. Um eine Paketquelle hinzuzufügen drücken Sie jetzt den Menüknopf und geben Sie deren Adresse an. -Die Adresse einer Paketquelle sieht etwa so aus: https://f-droid.org/repo +Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/repo Installiert Nicht installiert Hinzugefügt am %s @@ -59,7 +70,7 @@ Die Adresse einer Paketquelle sieht etwa so aus: https://f-droid.org/repoDiese Paketquelle ist bereits eingerichtet, dieses wird neue Schlüsselinformationen hinzuzufügen. Diese Paketquelle ist bereits eingerichtet, bestätigen, dass Sie diese wieder aktivieren möchten. Die eingehende Paketquelle ist bereits eingerichtet und aktiviert! - Sie müssen zuerst diese Paketquelle löschen, bevor Sie eine mit einem anderen Schlüssel hinzuzufügen! + Sie müssen diese Paketquelle zuerst löschen, bevor Sie eine mit einem anderen Schlüssel hinzuzufügen! Die Liste der genutzten Paketquellen hat sich geändert. Sollen diese aktualisiert werden? Paketquellen aktualisieren @@ -73,8 +84,8 @@ Sollen diese aktualisiert werden? Empfehlen Installieren Entfernen - Alle Aktualisierungen ignorieren - Diese Aktualisierung ignorieren + Alle Aktua. ignorieren + Diese Aktua. ignorieren Internetseite Probleme Quelltext @@ -88,15 +99,24 @@ Sollen diese aktualisiert werden? Diese Anwendung verfolgt und versendet Ihre Aktivitäten Diese Anwendung bewirbt nicht freie Erweiterungen Diese Anwendung bewirbt nicht freie Netzwerkdienste - Diese Anwendung hängt ab von nicht freien Anwendungen + Diese Anwendung hängt von nicht freien Anwendungen ab Der Originalcode ist nicht völlig frei Anzeige Experte + Zusätzliche Informationen anzeigen und zusätzliche Einstellungen aktivieren + Experteneinstellungen ausblenden Anwendungen suchen + Datenbanksynchronisierungsart Kompatibilität der Anwendung Inkompatible Versionen + Mit diesem Gerät inkompatible Anwendungsversionen anzeigen + Mit diesem Gerät inkompatible Anwendungsversionen ausblenden Root + Anwendungen, die Root-Rechte verlangen, nicht ausgrauen + Anwendungen ausgrauen, welche Root-Rechte verlangen Touchscreen ignorieren + Anwendungen, die einen Touchscreen benötigen, anzeigen + Anwendungen normal filtern Alle Was gibt es Neues Kürzlich Aktualisiert @@ -112,7 +132,44 @@ Sollen diese aktualisiert werden? Es werden keine Berechtigungen verwendet. Berechtigungen für Version %s Berechtigungen anzeigen + Eine Liste mit Berechtigungen, die von einer Anwendung verlangt werden, anzeigen + Zugriffsrechte nicht vor dem Herunterladen anzeigen Es ist keine Anwendung installiert, die mit %s umgehen kann Kompakte Ansicht + Symbole in einer kleineren Größe anzeigen + Symbole in normaler Größe anzeigen Thema + Nicht signiert + Adresse + Anwendungsanzahl + Signatur + Beschreibung + Letzte Aktualisierung + Aktualisierung + Name + Das bedeutet, dass die Liste von + Anwendungen nicht verifiziert werden konnte. + Sie sollten sorgsam mit Anwendungen, heruntergeladen + aus nicht nicht signierten Quellen, sein. + Diese Paketquelle wurde noch nicht benutzt. + Um beinhaltende Anwendungen anzuzeigen, muss diese Paketquelle + zuvor aktualisiert werden. + +Nach der Aktualisierung sind die Beschreibungen und andere Details + hier zu finden. + Möchten Sie die »{0}« + Paketquelle löschen, die {1} Anwendungen beinhaltet? Alle installierten Apps werden + NICHT entfernt. Allerdings werden diese nicht mehr über F-Droid aktualisiert. + Unbekannt + Paketquelle löschen? + Das Löschen einer Paketquelle bedeutet, + dass die Anwendungen daraus nicht mehr in F-Droid verfügbar sein werden. + +Bemerkung: Alle + zuvor installierten Anwendungen bleiben auf Ihrem Gerät. + »%1$s« deaktiviert. + +Sie müssen + diese Paketquelle wieder aktivieren, um Anwendungen daraus installieren zu können. + %s oder später diff --git a/res/values-el/array.xml b/res/values-el/array.xml index 2f02e1184..27b24978d 100644 --- a/res/values-el/array.xml +++ b/res/values-el/array.xml @@ -11,4 +11,9 @@ Σκοτεινό Φωτεινό + + Απενεργοποίηση (επισφαλής) + Κανονικό + Ολόκληρο + diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index 80f806187..e059b588e 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -29,7 +29,7 @@ Ένα αποθετήριο είναι μια πηγή εφαρμογών. Για να προσθέσετε κάποιο, πιέστε το πλήκτρο ΜΕΝΟΥ και εισάγετε το URL. -Μια διεύθυνση αποθετηρίου μοιάζει κάπως έτσι: https://f-droid.org/repo +Μια διεύθυνση αποθετηρίου μοιάζει κάπως έτσι: http://f-droid.org/repo Εγκατεστημένο Δεν είναι εγκατεστημένο Προστέθηκε στις %s @@ -47,7 +47,7 @@ %d διαθέσιμες ενημερώσεις. Διαθέσιμες ενημερώσεις για το F-Droid Παρακαλώ περιμένετε - Ενημέρωση λίστα εφαρμογών… + Ενημέρωση λίστα εφαρμογών... Λήψη εφαρμογών από Διεύθυνση αποθετηρίου Η λίστα με τα χρησιμοποιούμενα αποθετήρια έχει αλλάξει. @@ -82,6 +82,7 @@ Εμφάνιση Για Προχωρημένους Αναζήτηση εφαρμογών + Λειτουργία συγχρονισμόυ της βάσης δεδομένων Συμβατότητα εφαμοργής Μη συμβατές εκδόσεις Root @@ -97,7 +98,7 @@ %1$s Σύνδεση με %1$s - Έλεγχος συμβατότητας εφαρμογών με τη συσκευή σας… + Έλεγχος συμβατότητας εφαρμογών με τη συσκευή σας... Δεν χρησιμοποιείται καμία άδεια. Άδειες για την έκδοση %s Εμφάνιση αδειών diff --git a/res/values-eo/array.xml b/res/values-eo/array.xml index 79d432e35..fface9219 100644 --- a/res/values-eo/array.xml +++ b/res/values-eo/array.xml @@ -11,4 +11,9 @@ Dark Light + + Off (unsafe) + Normal + Full + diff --git a/res/values-es/array.xml b/res/values-es/array.xml index 49d24bead..ce0b75b74 100644 --- a/res/values-es/array.xml +++ b/res/values-es/array.xml @@ -11,4 +11,9 @@ Oscuro Claro + + Desactivado (peligroso) + Normal + Completo + diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index ea3c51d98..2e97bfe73 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -7,14 +7,25 @@ Parece que este paquete no es compatible con tu dispositivo. ¿Quieres probar e instalarlo de todos modos? Estás intentando instalar una versión inferior de esta aplicación. Hacerlo puede derivar en mal funcionamiento o incluso pérdida de datos. ¿Quieres intentarlo de todos modos? Versión + Editar + Borrar Caché de aplicaciones descargadas + Mantener en la tarjeta SD los ficheros apk descargados + No conservar ningún archivo apk Actualizaciones Otros Último escaneo del repositorio: %s nunca + Intervalo de actualización automática + No actualizar la lista de aplicaciones automáticamente Sólo con wifi + Actualizar la lista de aplicaciones automáticamente sólo con Wi-Fi + Actualizar la lista de aplicaciones automáticamente siempre Notificar + Notificar cuando hay actualizaciones + No notificar ninguna actualización Historial de actualizaciones + Días para considerar las aplicaciones nuevas o recientes: %s Resultados de la búsqueda Detalles de la aplicación No se encontró la aplicación @@ -39,6 +50,9 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Añadir nuevo repositorio Añadir Cancelar + Habilitar + Añadir clave + Sobreescribir Elige el repositorio a eliminar Actualizar repositorios Disponible @@ -47,9 +61,15 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo %d actualizaciones disponibles. Actualizaciones de F-Droid disponibles Por favor, espera - Actualizando la lista de aplicaciones… + Actualizando la lista de aplicaciones... Obteniendo la aplicación de Dirección del repositorio + Huella digital (opcional) + ¡Este repositorio ya existe! + Este repositorio ya está configurado, esto agregará nueva información sobre la clave. + Este repositorio ya está configurado, confirma que quieres volver a habilitarlo. + ¡El repositorio ya está configurado y habilitado! + ¡Debes borrar este repositorio antes de añadir uno con una clave diferente! La lista de repositorios usada ha cambiado. ¿Deseas actualizarlos? Actualizar repositorios @@ -79,13 +99,23 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Esta aplicación promueve complementos no libres Esta aplicación promueve servicios de red no libres Esta aplicación depende de otras no libres + El código fuente original no es totalmente libre Mostrar Experto + Mostrar info extra y habilitar los ajustes extra + Ocultar extras para usuarios experimentados Buscar aplicaciones + Modo síncrono de base de datos Compatibilidad de aplicaciones Versiones incompatibles + Mostrar las versiones de aplicaciones incompatibles con el dispositivo + Ocultar las versiones de aplicaciones no compatibles con el dispositivo Root + No marcar en gris las aplicaciones que requieren privilegios de root + Marcar en gris las aplicaciones que requieren privilegios de root Ignorar pantalla táctil + Incluir aplicaciones que requieran pantalla táctil siempre + Filtrar aplicaciones de manera normal Todos Novedades Recientemente actualizados @@ -97,11 +127,42 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo %1$s Conectando a %1$s - Comprobando la compatibilidad de las aplicaciones con tu dispositivo… + Comprobando la compatibilidad de las aplicaciones con tu dispositivo... No se usan permisos. Permisos para la versión %s Mostrar permisos + Mostrar una lista de los permisos que requiere una aplicación + No mostrar permisos antes de descargar No tienes instalada ninguna aplicación que pueda manejar %s Diseño compacto + Mostrar iconos en tamaño menor + Mostrar iconos en tamaño regular Tema + No firmado + URL + Número de aplicaciones + Firma + Descripción + Última actualización + Actualizar + Nombre + Esto significa que la lista de +aplicaciones no ha podido verificarse. +Deberías tener cuidado con aplicaciones descargadas desde índices no firmados. + Este repositorio aún no ha sido usado. +Para ver las aplicaciones que ofrece, necesitarás actualizarlo. +Una vez actualizado, la descripción y otros detalles estarán disponibles aquí. + ¿Quieres borrar el repositorio +\"{0}\", que contiene {1} aplicaciones? +Las aplicaciones instaladas NO se eliminarán, pero ya no podrás actualizarlas desde F-Droid. + Desconocido + ¿Borrar repositorio? + Borrar un repositorio significa +que sus aplicaciones ya no estarán disponibles en F-Droid. + +Nota: todas las aplicaciones previamente instaladas se quedarán en tu dispositivo. + \"%1$s\" deshabilitado. + +Necesitarás volver a habilitar este repositorio para instalar aplicaciones desde él. + %s o posterior diff --git a/res/values-eu/array.xml b/res/values-eu/array.xml index 975c0749d..80e6b4251 100644 --- a/res/values-eu/array.xml +++ b/res/values-eu/array.xml @@ -11,4 +11,9 @@ Dark Light + + Itzalita (ez da segurua) + Normala + Osoa + diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index f37301049..7c8e68156 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -35,7 +35,7 @@ GNU GPLv3 lizentziapean argitaratua. %d eguneraketa eskuragarri. F-Droid eguneraketak eskuragarri Mesedez itxaron - Aplikazio-zerrenda eguneratzen… + Aplikazio-zerrenda eguneratzen... Aplikazioa eskuratzen hemendik Biltegiaren helbidea Erabilitako biltegien zerrenda aldatu egin da. @@ -62,6 +62,7 @@ Eguneratu nahi dituzu? Bistaratu Aditua Bilatu aplikazioak + Datu-base modu sinkronoa Aplikazioen bateragarritasuna Root Ezikusi egin ukipen-pantailari @@ -70,7 +71,7 @@ Eguneratu nahi dituzu? Azkenaldian eguneratua %1$s(e)ra konektatzen - Aplikazioak zure gailuarekin bateragarriak diren egiaztatzen… + Aplikazioak zure gailuarekin bateragarriak diren egiaztatzen... Ez da baimenik erabiltzen. %s bertsioarentzako baimenak Erakutsi baimenak diff --git a/res/values-fa/array.xml b/res/values-fa/array.xml index f9d446d9a..14d3a0421 100644 --- a/res/values-fa/array.xml +++ b/res/values-fa/array.xml @@ -11,4 +11,9 @@ تاریک روشن + + خاموش (ناامن) + عادی + کامل + diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml index 9a6fa1356..e3b1a3c25 100644 --- a/res/values-fa/strings.xml +++ b/res/values-fa/strings.xml @@ -47,7 +47,7 @@ %d به‌روزرسانی موجود است. به‌روزرسانی‌های F-Droid موجود هستند لطفاً صبر کنید - به‌روزرسانی فهرست برنامه‌ها… + به‌روزرسانی فهرست برنامه‌ها... گرفتن برنامه از نشانی مخزن فهرست مخزن‌ها تغییر یافته‌است. @@ -82,6 +82,7 @@ نمایش خارج‌سازی جستجوی برنامه‌ها + حالت هماهنگی پایگاه داده‌ها هماهنگی برنامه نسخه‌های غیرهماهنگ روت @@ -97,7 +98,7 @@ %1$s اتصال به %1$s - بررسی سازگاری برنامه‌ها با دستگاه شما… + بررسی سازگاری برنامه‌ها با دستگاه شما... دسترسی‌ای استفاده نشده‌است. دسترسی‌های نسخهٔ %s نمایش دسترسی‌ها diff --git a/res/values-fi/array.xml b/res/values-fi/array.xml index 24139afd6..d4ef0d648 100644 --- a/res/values-fi/array.xml +++ b/res/values-fi/array.xml @@ -11,4 +11,9 @@ Tumma Valo + + Pois päältä (vaarallinen) + Normaali + Täysi + diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml index 26c27f90d..851070227 100644 --- a/res/values-fi/strings.xml +++ b/res/values-fi/strings.xml @@ -33,7 +33,7 @@ %d päivitystä saatavilla. F-Droid: Päivityksiä saatavilla Odota hetki - Päivitetään sovelluslistaa… + Päivitetään sovelluslistaa... Haetaan sovellusta lähteestä Sovelluslähteen osoite Lista käytetyistä sovelluslähteistä on muuttumut. @@ -61,6 +61,7 @@ Tahdotko päivittää ne? Näyttö Asiantuntija Etsi sovelluksia + Tietokannan synkronointi-tila Sovellusten yhteensopivuus Yhteensopimattomat versiot Root @@ -68,6 +69,6 @@ Tahdotko päivittää ne? Kaikki Uutta Viimeaikoina päivitetty - Tarkistetaan ohjelman yhteensopivuutta laitteesi kanssa… + Tarkistetaan ohjelman yhteensopivuutta laitteesi kanssa... Teema diff --git a/res/values-fr/array.xml b/res/values-fr/array.xml index 61b0d885a..d204ebad8 100644 --- a/res/values-fr/array.xml +++ b/res/values-fr/array.xml @@ -11,4 +11,9 @@ Sombre Clair + + Désactivé (non recommandé) + Normal + Complet + diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 2796c6b36..81c0a7b8c 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -29,7 +29,7 @@ Publiée sous licence GNU GPL v3. Un dépôt est une source d\'applications. Pour en ajouter un, appuyez maintenant sur le bouton MENU et entrez l\'adresse URL. -L\'URL d\'un dépôt ressemble à ceci : https://f-droid.org/repo +L\'URL d\'un dépôt ressemble à ceci : http://f-droid.org/repo Installée Pas installée Ajouté le %s @@ -47,7 +47,7 @@ L\'URL d\'un dépôt ressemble à ceci : https://f-droid.org/repo %d mises à jour sont disponibles. Des mises à jour F-Droid sont disponibles Patientez - Mise à jour de la liste d\'applications… + Mise à jour de la liste d\'applications... Réception d\'application de Adresse du dépôt La liste des dépôts utilisés a changé. @@ -82,6 +82,7 @@ Voulez-vous les mettre à jour ? Affichage Expert Rechercher des applications + Mode de synchronisation à la base de données Compatibilité de l\'application Versions incompatibles Root diff --git a/res/values-gl/array.xml b/res/values-gl/array.xml index 722add613..1770571ed 100644 --- a/res/values-gl/array.xml +++ b/res/values-gl/array.xml @@ -11,4 +11,9 @@ Escuro Claro + + Apagado (inseguro) + Normal + Completo + diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml index 0175f1829..eb8eb5551 100644 --- a/res/values-gl/strings.xml +++ b/res/values-gl/strings.xml @@ -51,7 +51,7 @@ Un enderezo a un repositorio sería algo %d actualizacións dispoñíbeis Actualizacións de F-Droid dispoñíbeis Agarde por favor - Actualizando a lista de aplicativos… + Actualizando a lista de aplicativos... Obtención do aplicativo desde Enderezo do repositorio Cambiou a lista de repositorios usados. @@ -86,6 +86,7 @@ Quere actualizalos? Amosar Experto Buscar aplicativos + Modo de sincronización da base de datos Compatibilidade de aplicativos Versións incompatíbeis Root diff --git a/res/values-gu/array.xml b/res/values-gu/array.xml index da5e2aa16..42eac981d 100644 --- a/res/values-gu/array.xml +++ b/res/values-gu/array.xml @@ -11,4 +11,9 @@ Dark Light + + બંધ (અસુરક્ષિત) + સામાન્ય + પૂર્ણ + diff --git a/res/values-it/array.xml b/res/values-it/array.xml index 2eb94a3d6..7c2f1ef01 100644 --- a/res/values-it/array.xml +++ b/res/values-it/array.xml @@ -11,4 +11,9 @@ Scuro Chiaro + + Disabilitato (non sicuro) + Normale + Completo + diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 6ce7c1d92..1d32fb822 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -7,14 +7,25 @@ Sembra che questo pacchetto non sia compatibile con il tuo dispositivo. Vuoi provare comunque ad installarlo? Stai provando a passare ad una versione precedente di questa applicazione. Potresti avere malfunzionamenti e perdita di dati. Vuoi installarla comunque? Versione - Cache applicazioni scaricate + Modifica + Elimina + Cache applicazioni + Conserva i file apk scaricati sulla scheda SD + Non conservare i file apk Aggiornamenti Altro Ultima scansione repository: %s mai + Intervallo degli aggiornamenti automatici + Non aggiornare automaticamente l\'elenco delle applicazioni Solo su wifi + Aggiorna automaticamente gli elenchi di applicazioni solo su wifi + Aggiorna sempre gli elenchi delle applicazioni automaticamente Avviso + Notifica gli aggiornamenti disponibili + Non notificare gli aggiornamenti Aggiorna i repository + Numero di giorni per considerare le applicazioni nuove o recenti: %s Risultati Ricerca Dettagli App Nessuna app corrispondente trovata @@ -39,6 +50,9 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo Aggiungi nuovo repository Aggiungi Annulla + Abilita + Aggiungi chiave + Sovrascrivi Rimuovi repository Aggiorna i repository Disponibile @@ -47,13 +61,19 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo %d aggiornamenti disponibili. Aggiornamenti per F-Droid Disponibili Attendere prego - Aggiornamento elenco applicazioni… + Aggiornamento elenco applicazioni... Scaricamento applicazione da Indirizzo repository + Impronta digitale (opzionale) + Questo repository esiste già! + Questo repository è già configurato, una nuova informazione chiave verrà aggiunta. + Questo repository è già configurato, conferma di volerlo abilitare nuovamente. + Il nuovo repository è già configurato e abilitato! + È necessario eliminare questo repository prima di poter aggiungerne uno con una chiave differente! L\'elenco dei repository in uso è cambiato. Vuoi aggiornarlo? Aggiorna i Repository - Gestione Repository + Gestione dei repository Preferenze Informazioni Cerca @@ -79,13 +99,23 @@ Vuoi aggiornarlo? Questa app promuove add-on non liberi Questa app promuove servizi di rete non liberi Questa app dipende da applicazioni non libere + Il codice sorgente a monte non è completamente libero Mostra Esperto + Mostra le informazioni ulteriori e abilita le impostazioni avanzate + Nascondi le opzioni avanzate per utenti esperti Ricerca applicazioni + Modalità di sincronizzazione database Compatibilità applicazioni Versioni incompatibili + Mostra le versioni incompatibili con il dispositivo + Nascondi le versioni incompatibili con il dispositivo Amministratore + Non disabilitare le applicazioni che richiedono privilegi di root + Disabilita le applicazioni che richiedono privilegi di root Ignora il Touchscreen + Includi sempre le applicazioni con touchscreen obbligatorio + Filtra le applicazioni normalmente Tutte Novità Aggiornate di Recente @@ -97,11 +127,35 @@ Vuoi aggiornarlo? %1$s Connessione a %1$s - Controllo compatibilità applicazioni con il tuo dispositivo… + Controllo compatibilità applicazioni con il tuo dispositivo... Non viene usata alcuna autorizzazione. Autorizzazioni per la versione %s Mostra autorizzazioni + Mostra l\'elenco dei permessi richiesti da un\'applicazione + Non mostrare i permessi prima del download Non hai alcuna app disponibile che può gestire %s Layout Compatto + Mostra le icone a dimensione ridotta + Mostra le icone a dimensione normale Tema + Non firmato + URL + Numero di applicazioni + Firma + Descrizione + Ultimo aggiornamento + Aggiorna + Nome + Ciò significa che l\'elenco di applicazioni non potrà essere verificato. È consigliabile prestare attenzione alle applicazioni scaricate da indici non firmati. + Questo repository non è ancora stato utilizzato. +Per poter vedere le applicazioni contenute, è necessario aggiornarlo. +Una volta aggiornato, la descrizione e le altre informazioni saranno disponibili qui. + Si desidera eliminare il \"{0}\" repository, che contiene {1} applicazioni? Le applicazioni installate NON saranno rimosse, ma non sarà più possibile aggiornarle attraverso F-Droid. + Sconosciuto + Eliminare il repository? + Eliminare un repository implica che le applicazioni contenute non saranno più disponibili attraverso F-Droid. +Nota: Tutte le applicazioni installate precedentemente rimarranno sul dispositivo. + \"%1$s\" è disabilitato. +È necessario abilitare nuovamente questo repository per installare le applicazioni contenute. + %s o successivi diff --git a/res/values-ko/array.xml b/res/values-ko/array.xml index d00633ed0..2bf47cb19 100644 --- a/res/values-ko/array.xml +++ b/res/values-ko/array.xml @@ -11,4 +11,9 @@ 어두운 밝은 + + 해제 (안전하지 않음) + 보통 + 전체 + diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index 5baeb291d..59536b009 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -39,7 +39,7 @@ %d개의 업데이트를 사용할 수 있습니다. F-Droid 업데이트를 사용할 수 있습니다. 잠시만 기다려주세요 - 응용 프로그램 목록 업데이트중… + 응용 프로그램 목록 업데이트중... 에서 응용프로그램 가져오기 저장소 주소 사용된 저장소의 목록이 변경되었습니다. @@ -70,6 +70,7 @@ 표시 전문가 응용 프로그램 검색 + 데이터베이스 동기화 모드 응용 프로그램 호환성 호환되지 않는 버전 터치스크린 무시 diff --git a/res/values-nb/array.xml b/res/values-nb/array.xml index bdf6445f2..2fb6485b1 100644 --- a/res/values-nb/array.xml +++ b/res/values-nb/array.xml @@ -11,4 +11,9 @@ Mørk Lys + + Av (utrygt) + Normalt + Fullt + diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index 02159393f..d8309655f 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -43,7 +43,7 @@ Lisensiert GNU GPLv3. %d oppdateringer tilgjengelig. F-Droid: Oppdateringer tilgjengelig Vennligst vent - Oppdaterer applikasjonsliste… + Oppdaterer applikasjonsliste... Henter program fra Registeradresse Listen over brukte register har endret seg. Vil du oppdatere dem? @@ -77,6 +77,7 @@ Lisensiert GNU GPLv3. Vis Ekspert Søk i programliste + Modus for databasesynkronisering Programstøtte Ukompatible versjoner Rot @@ -92,7 +93,7 @@ Lisensiert GNU GPLv3. %1$s Kobler til %1$s - Sjekker programstøtte for ditt utstyr… + Sjekker programstøtte for ditt utstyr... Krever ingen tillatelser. Tillatelser for versjon %s Vis tillatelser diff --git a/res/values-nl/array.xml b/res/values-nl/array.xml index 765ae052b..19c7f0690 100644 --- a/res/values-nl/array.xml +++ b/res/values-nl/array.xml @@ -11,4 +11,9 @@ Donker Licht + + Uit (onveilig) + Normaal + Vol + diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index 2d3539b10..c03571660 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -7,7 +7,11 @@ Dit pakket is niet verenigbaar met uw apparaat. Wilt u het alsnog proberen te installeren? U probeert deze applicatie te degraderen naar een oudere versie. Als u dit doet uw data kan corrupt of verloren raken. Wilt u dit alsnog uitvoeren? Versie + Bewerken + Verwijderen buffer gedownloade apps + Gedownloade apk-files bewaren op SD card + apk-files niet bewaren Updates Andere Laatste bronnen scan: %s @@ -42,15 +46,16 @@ Een bron-adres ziet er ongeveer Voeg nieuwe bron toe Toevoegen Annuleren + Overschrijven Kies bron om te verwijderen Vernieuw bronnen Beschikbaar Updates - 1 vernieuwing is beschikbaar - %d vernieuwingen zijn beschikbaar - F-Droid Vernieuwingen Beschikbaar + 1 update beschikbaar + %d updates beschikbaar + F-Droid update beschikbaar Even geduld aub - Applicatie-lijst vernieuwen… + Applicatie-lijst vernieuwen... downloaden applicatie van Bron-adres De lijst van gebruikte bronnen is veranderd. @@ -66,8 +71,8 @@ Wilt u ze vernieuwen? Delen Installeren Deinstalleren - Negeer Alle Verbeteringen - Negeer Deze Verbetering + Negeer Alle Updates + Negeer Deze Update Website Problemen Broncode @@ -85,6 +90,7 @@ Wilt u ze vernieuwen? Laat zien Expert Zoek applicaties + Database sync-modus Applicatie verenigbaarheid Onverenigbare versies Root @@ -98,13 +104,18 @@ Wilt u ze vernieuwen? Verwerken applicatie %2$d van %3$d van %1$s - Connecteren naar -%1$s - Controleer app compatibiliteit met uw apparaat… + Verbinden met %1$s + Controleer app compatibiliteit met uw apparaat... Geen permissies worden gebruikt Permissies voor versie %s Laat permissies zien U hebt geen beschikbare app die %s kan verwerken Compacte Layout Thema + URL + Aantal apps + Beschrijving + Meest recente update + Naam + Onbekend diff --git a/res/values-pl/array.xml b/res/values-pl/array.xml index e6076d5e5..103711e39 100644 --- a/res/values-pl/array.xml +++ b/res/values-pl/array.xml @@ -11,4 +11,9 @@ Ciemny Jasny + + Wyłączone (niebezpieczne) + Normalny + Pełny + diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 155d056e7..121c09c6a 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -4,17 +4,30 @@ Znaleziono jedną pasującą aplikację \'%s\': Nie znaleziono żadnych pasujących aplikacji \'%s\' Nowa wersja jest podpisana innym kluczem niż poprzednia. Aby ją zainstalować należy najpierw usunąć tę starą. Zrób to i spróbuj ponownie. (Proszę pamiętać, że deinstalacja spowoduje usunięcie wszystkich danych przechowywanych przez aplikację) - Wygląda na to, że ta aplikacja nie jest kompatybilna z twoim urządzeniem. Spróbować mimo to? + Wygląda na to, że ta aplikacja nie jest kompatybilna z twoim urządzeniem. Zainstalować mimo to? + Próbujesz zainstalować starszą wersję tej aplikacji.Może to spowodować nieprawidłowe działanie, a nawet utratę swoich danych. Czy na pewno chcesz spróbować i zainstalować starszą wersję ? Wersja + Edytuj + Usuń Buforuj pobrane aplikacje + Trzymaj ściągnięte pliki apk na karcie SD + Nie zachowuj żadnych apk Aktualizacje Inne Ostatnie uaktualnienie listy aplikacji: %s nigdy + Automatyczna przerwa aktualizacji + Żadnych automatycznych aktualizacji listy aplikacji Tylko przez wifi + Aktualizuj automatycznie tylko przez wifi + Zawsze aktualizuj listę aplikacji automatycznie Powiadom + Powiadom kiedy aktualizacje są dostępne + Nie powiadamiaj o aktualizacjach Historia aktualizacji + Liczba dni, aby uznać aplikację za nową lub niedawną: %s Wyniki wyszukiwania + Szczegóły aplikacji Nie znaleziono takiej aplikacji O F-Droid Oryginalnie bazowany na Aptoide. @@ -23,6 +36,11 @@ Wydany na licencji GNU GPLv3. Email: Wersja: Strona internetowa + Nie masz skonfigurowanych żadnych repozytoriów! + +Repozytorium jest źródłem aplikacji. Aby dodać jedno wciśnij MENU i wprowadź adres URL. + +Przykładowy adres repozytorium: https://f-droid.org/repo Zainstalowano Niezainstalowane Dodano %s @@ -32,6 +50,7 @@ Wydany na licencji GNU GPLv3. Dodaj nowe repozytorium Dodaj Anuluj + Włącz Dodaj klucz Nadpisz Wybierz repozytorium które chcesz usunąć @@ -42,11 +61,15 @@ Wydany na licencji GNU GPLv3. Dostępnych jest %d uaktualnień Uaktualnienie F-Droid jest dostępne Proszę czekać - Aktualizowanie listy aplikacji… + Aktualizowanie listy aplikacji... Pobieranie aplikacji z Adres repozytorium - odcisk (opcjonalnie) + Odcisk palca (opcjonalnie) Repozytorium już istnieje! + To repozytorium jest już ustawione, doda to nowe kluczowe informacje + To repozytorium jest już ustawione, potwierdź że chcesz je włączyć ponownie + Nadchodzące repozytorium jest już ustawione i włączone! + Musisz najpierw usunąć to repozytorium zanim dodasz następne z innym kluczem! Lista wykorzystywanych repozytoriów uległa zmianie. Czy chcesz je zaktualizować? Aktualizuj repozytoria @@ -74,12 +97,25 @@ Czy chcesz je zaktualizować? Ta aplikacja zawiera reklamy Ta aplikacja śledzi twoje działania Ta aplikacja promuje niewolne dodatki - Ta aplikacja promuje niewolne usługi + Ta aplikacja promuje niewolne usługi sieciowe Ta aplikacja wymaga innych, niewolnych aplikacji + Upstreamowy kod źródłowy nie jest w pełni wolny + Wyświetl Ekspert + Pokaż dodatkowe informacje i włącz dodatkowe ustawienia + Ukryj dodatki dla zaawansowanych użytkowników Wyszukaj aplikacje + Tryb synchronizacji bazy danych Kompatybilność aplikacji + Niekompatybilne wersje + Pokaż wersje aplikacji niekompatybilne z urządzeniem + Ukryj wersje aplikacji niekompatybilnych z urządzeniem Root + Nie przyciemniaj aplikacji wymagających uprawnień roota + Przyciemnij aplikacje wymagające uprawnień roota + Ignoruj ekran dotykowy + Zawsze uwzględniaj aplikacje, które wymagają ekranu dotykowego + Filtruj aplikacje normalnie Wszystkie Co nowego Ostatnio zaktualizowane @@ -87,9 +123,37 @@ Czy chcesz je zaktualizować? Przetwarzanie aplikacji %2$d / %3$d z %1$s Trwa łączenie z %1$s - Sprawdzanie kompatybilności aplikacji z urządzeniem… + Sprawdzanie kompatybilności aplikacji z urządzeniem... + Brak użytych uprawnień. Uprawnienia dla wersji %s Wyświetl uprawnienia + Wyświetl listę uprawnień wymaganych przez aplikację + Nie pokazuj uprawnień przed pobraniem + Nie masz żadnej dostępnej aplikacji, która może obsłużyć %s Widok kompaktowy + Pokaż ikony w mniejszym rozmiarze + Pokaż ikony w zwykłym rozmiarze Motyw + Niepodpisany + URL + Liczba aplikacji + Podpis + Opis + Ostatnia modyfikacja + Aktualizacja + Nazwa + To oznacza, że lista aplikacji nie może być zweryfikowana. Powinieneś być ostrożny przy aplikacjach ściągniętych z nie podpisanych źródeł + To repozytorium było jeszcze używane. Aby wyświetlić jego aplikacje musisz je zaktualizować. + +Opis i inne szczegóły będą tu dostępne gdy zaktualizujesz repozytorium + Czy chcesz usunąć repozytorium \"{0}\", zawierające \"{1}\" aplikacji? Zainstalowane aplikacje nie zostaną usunięte, ale nie będziesz mógł ich dłużej aktualizować przez F-Droid + Nieznany + Usunąć repozytorium? + Usunięcie repozytorium oznacza, że aplikacje z niego nie będą dłużej dostępne w F-Droid. + +Uwaga: Wszystkie poprzednio zainstalowane aplikacje zostaną na urządzeniu. + Zablokowane \"%1%s\". + +Aby zainstalować aplikacje z tego repozytorium musisz je włączyć ponownie. + %s lub później diff --git a/res/values-pt-rBR/array.xml b/res/values-pt-rBR/array.xml index 1614797d3..6fc95972c 100644 --- a/res/values-pt-rBR/array.xml +++ b/res/values-pt-rBR/array.xml @@ -11,4 +11,9 @@ Escuro Claro + + Desligada (inseguro) + Normal + Completa + diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index 72164cc15..fa3159c33 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -29,7 +29,7 @@ Lançado sob a licença GNU GPLv3. Um repositório é uma fonte de aplicativos. Para adicionar um, pressione o botão MENU e digite a URL. -Um endereço do repositório é algo similar a isto: https://f-droid.org/repo +Um endereço do repositório é algo similar a isto: http://f-droid.org/repo Instalado Não Instalado Adicionado em %s @@ -47,7 +47,7 @@ Um endereço do repositório é algo similar a isto: https://f-droid.org/repo%d atualizações disponíveis. Atualizações do F-Droid Disponíveis Aguarde - Atualizando a lista de aplicativos… + Atualizando a lista de aplicativos... Baixando aplicativo de Endereço do repositório A lista de repositórios usados mudou. @@ -82,6 +82,7 @@ Você deseja atualizá-los? Exibição Especialista Pesquisar aplicativos + Modo de sincronia do banco de dados Compatibilidade de aplicativo Versões incompatíveis Root @@ -97,7 +98,7 @@ Você deseja atualizá-los? %1$s Conectando-se a %1$s - Verificando compatibilidade de aplicativos com o seu dispositivo… + Verificando compatibilidade de aplicativos com o seu dispositivo... Nenhuma permissão utilizada. Permissões para a versão %s Mostrar permissões diff --git a/res/values-qqq/strings.xml b/res/values-qqq/strings.xml new file mode 100644 index 000000000..0b943988a --- /dev/null +++ b/res/values-qqq/strings.xml @@ -0,0 +1,5 @@ + + + »Alle Aktualisierungen ignorieren« wird nicht richtig dargestellt. Sowohl horizontal als auch vertikal. Deshalb abgekürzte Wörter. Etwa 23 Zeichen stehen zur Verfügung. Ak·tu·a·li·sie·ren, ig·no·rie·ren + »Alle Aktualisierungen ignorieren« wird nicht richtig dargestellt. Sowohl horizontal als auch vertikal. Deshalb abgekürzte Wörter. Etwa 23 Zeichen stehen zur Verfügung. Ak·tu·a·li·sie·rung, ig·no·rie·ren + diff --git a/res/values-ro/array.xml b/res/values-ro/array.xml index 8c1fabb4e..d8e58ad04 100644 --- a/res/values-ro/array.xml +++ b/res/values-ro/array.xml @@ -11,4 +11,9 @@ Dark Light + + Inchis (nerecomandat) + Normal + Complet + diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml index f0f0db7ae..8a2bc508f 100644 --- a/res/values-ro/strings.xml +++ b/res/values-ro/strings.xml @@ -1,8 +1,8 @@ - Sa gasit %1$d aplicații potrivita cu \'%2$s\' - Sa gasit o aplicatie potrivita cu \'%s\' - Nu exita aplicatii potrivite cu \'%s\': + Sa gasit o aplicatie potrivita cu %s\' + Sa gasit o aplicatie potrivita cu %s\' + Nu exita aplicatii potrivite cu %s\': Versiune Istoric aplicatii descarcate Noutati @@ -25,6 +25,6 @@ Distribuit sub licenta GNU GPLv3. Actualizare depozit aplicatii Disponibil Actualizare - Asteptati … - Se actualizeaza lista … + Asteptati ... + Se actualizeaza lista ... diff --git a/res/values-ru/array.xml b/res/values-ru/array.xml index 8b39f1618..13a7defd0 100644 --- a/res/values-ru/array.xml +++ b/res/values-ru/array.xml @@ -11,4 +11,9 @@ Dark Light + + Откл. (опасно) + Обычный + Полный + diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index ccb7394b6..face682fe 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -4,14 +4,28 @@ Найдено одно совпадение с \'%s\': Не найдено ни одного совпадения с \'%s\' Новая версия подписана ключом отличным от старого. Для установки новой версии, сначала нужно удалить старую программы. А потом попробовать снова. (Замечание: при удалении программы будут удалены все её данные) + Похоже, что этот пакет не несовместим с Вашим устройством. Вы все ещё хотите попытаться установить его? Вы пытаетесь установить более старую версию приложения. Это может привести к его некорректной работе и даже потере данных. Вы уверены, что хотите продолжить? Версия - Кешировать загруженные приложения + Редактировать + Удалить + Кэшировать загруженные приложения + Хранить загруженные файлы apk на SD-карте + Не хранить файлы apk Обновления + Другой Обновлено: %s никогда + Интервал автоматического обновления + Не обновлять автоматически список приложений + Только по Wi-Fi + Обновлять списки приложений автоматически только по Wi-Fi + Всегда автоматически обновлять списки приложений Уведомление + Сообщать о наличии обновлений + Не уведомлять о любых обновлениях История обновлений + Количество дней, в которые отображаются новые или недавно измененные приложения: %s Результаты поиска Описание приложения Приложение не найдено @@ -36,20 +50,30 @@ Добавить репозиторий Добавить Отменить + Включить + Добавить ключ + Перезаписать Удалить репозиторий Обновить репозитории Доступно Обновления Доступно 1 обновление. Обновлений доступно - %d. + Доступны обновления для F-Droid Подождите - Список приложений обновляется… + Список приложений обновляется... Взять приложение из Адрес репозитория + Отпечаток ключа (опционально) + Этот репозиторий уже существует! + Этот репозиторий уже установлен, это действие добавит новую информацию о ключе. + Этот репозиторий уже настроен, убедитесь, что вы хотите снова включить его. + Репозиторий уже настроен и включен! + Вы должны удалить этот репозиторий, прежде чем вы можете добавить новый с другим ключом! Список репозиториев изменился. Обновить его? Обновить репозитории - Редактировать репозитории + Репозитории Настройки О программе Поиск @@ -59,31 +83,77 @@ Поделиться Установить Удалить + Игнорировать все обновления + Игнорировать данное обновление Сайт Ошибки Исходный код + Обновление Пожертвовать Версия %s установлена Не установлено Загруженный файл повреждён Загрузка остановлена + Это приложение содержит рекламу + Это приложение будет отслеживать ваши действия и сообщать о вашей деятельности + Это приложение предлагает использовать несвободные дополнения + Это приложение предлагает использовать несвободные сетевые услуги + Это приложение зависит от других несвободных приложений + Оригинальный исходный код не является полностью свободным Вид Эксперт + Показать дополнительную информацию и включить дополнительные настройки + Скрыть дополнительные возможности для опытных пользователей Найти приложения + Режим синхронизации базы Совместимость приложений + Несовместимые версии + Показать версии программ, несовместимые с устройством + Скрыть версии программ, несовместимые с устройством Суперпользователь + Не выделять серым цветом приложения, требующие привилегии суперпользователя + Выделять серым цветом приложения, требующие привилегии суперпользователя Игнорировать Тачскрин + Всегда включать приложения, которые требуют сенсорный экран + Фильтровать приложения как обычно Все Что Нового Недавно обновлённые Загрузка %2$s / %3$s (%4$d%%) из +%1$s + Обработка приложения +%2$d из %3$d от %1$s Соединение с %1$s - Проверка совместимости приложений с устройством… + Проверка совместимости приложений с устройством... Разрешений не требуется. Разрешения для версии %s Показывать разрешения + Отображение списка разрешений, которые требуются приложению + Не показывать разрешения перед загрузкой + У вас нет установленного приложения для обработки %s Компактный вид + Показать иконки в меньшем размере + Показать иконки обычного размера + Тема + Неподписанный + URL + Количество приложений + Подпись + Описание + Последние обновление + Обновить + Название + Это означает, что список приложений не может быть проверен. Вы должны быть осторожны с приложениями, загруженными из неподписанных источников. + Этот репозиторий еще не был использован. Для того чтобы просмотреть приложения, которые он предоставляет, вы должны обновить его. После обновления, описание и другие детали станут доступны. + Вы хотите удалить \"{0}\" репозиториев, который имеет {1} приложений в нем? Установленные приложения не будут удалены, но вы больше не сможете обновить их через F-Droid. + Неизвестный + Удалить репозиторий? + Удаление репозитория означает что приложения из него больше не будут доступны в F-Droid. + +Примечание: Все ранее установленные приложения будут оставаться на вашем устройстве. + \"%1$s\" отключен. Вам нужно повторно включить этот репозиторий для установки приложений из него. + %s или позднее diff --git a/res/values-sl/array.xml b/res/values-sl/array.xml index bc3404374..ba22674f8 100644 --- a/res/values-sl/array.xml +++ b/res/values-sl/array.xml @@ -11,4 +11,9 @@ Dark Light + + Izključeno (ni varno) + Običajno + Polno + diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index 8f98a56bb..73ce9a5ed 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -28,7 +28,7 @@ Izdan z licenco GNU GPLv3. Na razpolago Posodobitve Počakajte prosim - Poteka posodobitev spiska aplikacij … + Poteka posodobitev spiska aplikacij ... Prejem aplikacije iz Naslov skladišča Spisek uporabljenih skladišč se je spremenil. @@ -52,6 +52,7 @@ Ga želite posodobiti? Tämä ohjelma sisältää mainontaa Napredno Iskanje aplikacij + Način sinhronizacije baze podatkov Združljivost aplikacij Yhteensopimattomat versiot Skrbnik diff --git a/res/values-sr/array.xml b/res/values-sr/array.xml index 7d414c084..0a0c851a6 100644 --- a/res/values-sr/array.xml +++ b/res/values-sr/array.xml @@ -11,4 +11,9 @@ Dark Light + + Искључено (није безбедно) + Нормално + Пуно + diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index faf2f69a6..b4fa35243 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -29,7 +29,7 @@ Ризнице су места одакле се скидају апликације. Да би сте додали једну, притисните тастер МЕНИ и унесите адресу. -Адреса ризнице би личила на ово: https://f-droid.org/repo +Адреса ризнице би личила на ово: http://f-droid.org/repo Инсталирана Није Инсталирана Додато %s @@ -47,7 +47,7 @@ %d нове/нових верзија на располагању Ажурирање Ф-Дроида на располагању. Сачекајте - Ажурира се листа апликација… + Ажурира се листа апликација... Скида се апликација са Адреса ризнице Промењена је листа ризница у употреби. @@ -81,6 +81,7 @@ Прикажи Стручни Претрага апликација + Режим синхронизације базе података Компатибилност апликације Рут Игнориши Додирни Екран @@ -95,7 +96,7 @@ %1$s Повезивање са %1$s - Проверава се да ли је апликација компатибилна са вашим уређајем… + Проверава се да ли је апликација компатибилна са вашим уређајем... Не захтевају се никакве дозволе. Дозволе за верзију %s Прикажи дозволе diff --git a/res/values-sv/array.xml b/res/values-sv/array.xml index 12b70f0e9..9edfdb38c 100644 --- a/res/values-sv/array.xml +++ b/res/values-sv/array.xml @@ -11,4 +11,9 @@ Mörk Ljus + + Av (osäkert) + Normal + Fullständig + diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index 58cc6cdec..6cf446b9c 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -7,14 +7,25 @@ Det verkar som att detta program inte är kompatibelt med enheten. Vill ni försöka installera det ändå? Du försöker nedgradera detta program. Detta kan få det att fungera felaktigt eller orsaka förlust av dina data. Vill du ändå försöka nedgradera? Version + Redigera + Ta bort Cacha nerladdade appar + Behåll hämtade apk-filer på SD-kort + Behåll inga apk-filer Uppdateringar Andra Senaste förrådsavsökning: %s aldrig + Automatiskt uppdateringsintervall + Inga automatiska uppdateringar av applistor Endast via WiFi + Uppdatera applistor automatiskt endast över wifi + Uppdatera alltid applistor automatiskt Avisering + Meddela när uppdateringar finns tillgängliga + Meddela inte om uppdateringar Uppdateringshistorik + Antal dagar som appar betraktas som nya: %s Sökresultat Appdetaljer Ingen sådan app funnen @@ -50,7 +61,7 @@ En förrådsadress ser ut så här: https://f-droid.org/repo %d uppdateringar finns tillgängliga. Uppdateringar för F-Droid tillgängliga Var vänlig vänta - Uppdaterar programlistan… + Uppdaterar programlistan... Hämtar program från Förrådsadress fingeravtryck (valfritt) @@ -91,11 +102,20 @@ Vill du uppdatera dem? Källkoden från uppströms är inte fullständigt fri Visning Expert + Visa extra info och aktivera extra inställningar + Dölj extraval för vana användare Sök program + Databassynkroniseringsläge Programkompatibilitet Inkompatibla versioner + Visa appversioner som är inkompatibla med enheten + Dölj appversioner som är inkompatibla med enheten Root + Gråtona inte appar som kräver root-behörighet + Gråtona appar som kräver root-behörighet Ignorera touchscreen + Inkludera alltid appar som kräver touchscreen + Filtrera appar normalt Alla Nyheter Nyligt uppdaterade @@ -111,7 +131,27 @@ Vill du uppdatera dem? Inga behörigheter används. Behörigheter för version %s Visa behörigheter + Visa en lista över behörigheter en app behöver + Visa inte behörigheter innan hämtning Du har inte någon app tillgänglig för hantering av %s Kompakt layout + Visa ikoner i en mindre storlek + Visa ikoner i normal storlek Tema + Osignerad + URL + Antal appar + Signatur + Beskrivning + Senaste uppdatering + Uppdatera + Namn + Detta betyder att listan av program inte kunde verifieras. Du bör vara försiktig med program som hämtats från osignerade index. + Detta förråd har inte använts än. För att se de appar det erbjuder måste du uppdatera det. Då det uppdaterats kommer beskrivning och övriga detaljer finnas tillgängliga här. + Vill du ta bort förrådet \"{0}\" , som innehåller {1} appar? Installerade appar tas INTE bort, men du kommer inte längre kunna uppdatera dem genom F-Droid. + Okänd + Ta bort förråd? + Då ett förråd tas bort kommer inte längre appar därifrån vara tillgängliga via F-Droid. Obs: Alla tidigare installerade appar kommer finnas kvar på din enhet. + Avaktiverade \"%1$s\". Du kommer behöva återaktivera detta förråd för att kunna installera appar från det. + %s eller senare diff --git a/res/values-tr/array.xml b/res/values-tr/array.xml index 73ba4a61a..b7840e71e 100644 --- a/res/values-tr/array.xml +++ b/res/values-tr/array.xml @@ -11,4 +11,9 @@ Koyu Açık + + Devre dışı (güvenli değildir) + Normal + Tümü + diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml index 28aaf249d..746a0b890 100644 --- a/res/values-tr/strings.xml +++ b/res/values-tr/strings.xml @@ -47,7 +47,7 @@ Bir depo adresi şuna benzer: https://f-droid.org/repo %d güncelleme bulunmaktadır. F-Droid güncellemeleri bulunmaktadır Bekleyiniz - Uygulama listesi güncelleniyor… + Uygulama listesi güncelleniyor... Uygulama buradan alınıyor: Depo adresi Kullanılan depoların listesi değişti. @@ -82,6 +82,7 @@ Güncellemek ister misiniz? Görüntüleme Uzman Uygulama ara + Veritabanı eşleşme modu Uygulama uyumu Uyumsuz sürümler Root diff --git a/res/values-ug/array.xml b/res/values-ug/array.xml index 25e167824..35b79432f 100644 --- a/res/values-ug/array.xml +++ b/res/values-ug/array.xml @@ -11,4 +11,9 @@ قاراڭغۇ يورۇق + + تاقاق (بىخەتەر ئەمەس) + نورمال + تولۇق + diff --git a/res/values-ug/strings.xml b/res/values-ug/strings.xml index 0b1921925..ffb278803 100644 --- a/res/values-ug/strings.xml +++ b/res/values-ug/strings.xml @@ -29,7 +29,7 @@ خەزىنە ئەپلەرنىڭ تارقىتىلىش مەنبەسى بولۇپ، مەنبە قوشۇشتا، تىزىملىك توپچىنى بېسىپ، ئاندىن URLنى كىرگۈزۈڭ. -خەزىنە ئادرېسى بۇنىڭغا ئوخشاش بولىدۇ: https://f-droid.org/repo +خەزىنە ئادرېسى بۇنىڭغا ئوخشاش بولىدۇ: http://f-droid.org/repo ئورنىتىلغان ئورنىتىلمىغان %s دا قوشۇلغان @@ -82,6 +82,7 @@ كۆرسەت ئالىي ئەپ ئىزدە + ساندان قەدەمداش ھالەت ئەپ ماسلىشىشچانلىقى ماسلاشمايدىغان نەشرىلىرى Root diff --git a/res/values-uk/array.xml b/res/values-uk/array.xml index 83ea4a1e8..0d43d54c8 100644 --- a/res/values-uk/array.xml +++ b/res/values-uk/array.xml @@ -11,4 +11,9 @@ Dark Light + + Ніколи (небезпечно) + Типово + Повністю + diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index d9af9a175..15ed67ee2 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -28,7 +28,7 @@ Наявне Оновлення Зачекайте - Оновлюю список програм… + Оновлюю список програм... Звантажую програму Адреса репозиторію Список репозиторіїв змінено. @@ -53,6 +53,7 @@ Звантаження скасовано Експерт Пошук програм + Синхронізація БД Сумісність Суперкористувач Ігнорувати тачскрін diff --git a/res/values-zh-rCN/array.xml b/res/values-zh-rCN/array.xml index dd10ac48a..120aefcf0 100644 --- a/res/values-zh-rCN/array.xml +++ b/res/values-zh-rCN/array.xml @@ -11,4 +11,9 @@ Dark Light + + 关闭(存在安全风险) + 正常 + 完整的 + diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index b12da2de2..80451bdda 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -53,6 +53,7 @@ 下载取消 高级 搜索应用 + 数据同步模式 应用兼容性 Root 忽略需要触屏的应用 From f7051a3f50acbeb442514519db60bba29bba10ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 5 Mar 2014 22:58:38 +0100 Subject: [PATCH 166/282] Run remove-unused-trans again --- res/values-ar/array.xml | 5 ----- res/values-bg/array.xml | 5 ----- res/values-bg/strings.xml | 1 - res/values-ca/array.xml | 5 ----- res/values-ca/strings.xml | 1 - res/values-de/array.xml | 5 ----- res/values-de/strings.xml | 2 -- res/values-el/array.xml | 5 ----- res/values-el/strings.xml | 1 - res/values-eo/array.xml | 5 ----- res/values-es/array.xml | 5 ----- res/values-es/strings.xml | 2 -- res/values-eu/array.xml | 5 ----- res/values-eu/strings.xml | 1 - res/values-fa/array.xml | 5 ----- res/values-fa/strings.xml | 1 - res/values-fi/array.xml | 5 ----- res/values-fi/strings.xml | 1 - res/values-fr/array.xml | 5 ----- res/values-fr/strings.xml | 1 - res/values-gl/array.xml | 5 ----- res/values-gl/strings.xml | 1 - res/values-gu/array.xml | 5 ----- res/values-it/array.xml | 5 ----- res/values-it/strings.xml | 2 -- res/values-ko/array.xml | 5 ----- res/values-ko/strings.xml | 1 - res/values-nb/array.xml | 5 ----- res/values-nb/strings.xml | 1 - res/values-nl/array.xml | 5 ----- res/values-nl/strings.xml | 1 - res/values-pl/array.xml | 5 ----- res/values-pl/strings.xml | 2 -- res/values-pt-rBR/array.xml | 5 ----- res/values-pt-rBR/strings.xml | 1 - res/values-ro/array.xml | 5 ----- res/values-ru/array.xml | 5 ----- res/values-ru/strings.xml | 2 -- res/values-sl/array.xml | 5 ----- res/values-sl/strings.xml | 1 - res/values-sr/array.xml | 5 ----- res/values-sr/strings.xml | 1 - res/values-sv/array.xml | 5 ----- res/values-sv/strings.xml | 2 -- res/values-tr/array.xml | 5 ----- res/values-tr/strings.xml | 1 - res/values-ug/array.xml | 5 ----- res/values-ug/strings.xml | 1 - res/values-uk/array.xml | 5 ----- res/values-uk/strings.xml | 1 - res/values-zh-rCN/array.xml | 5 ----- res/values-zh-rCN/strings.xml | 1 - 52 files changed, 170 deletions(-) diff --git a/res/values-ar/array.xml b/res/values-ar/array.xml index 6b1fbe25d..f26ac258c 100644 --- a/res/values-ar/array.xml +++ b/res/values-ar/array.xml @@ -11,9 +11,4 @@ غامق فاتح - - معطل (غير آمن) - عادي - مكتمل - diff --git a/res/values-bg/array.xml b/res/values-bg/array.xml index d7738bcf9..e81f9ad28 100644 --- a/res/values-bg/array.xml +++ b/res/values-bg/array.xml @@ -11,9 +11,4 @@ Dark Light - - Изключено (опасно) - Нормално - Пълно - diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml index 8ae9aa460..c49dd2e83 100644 --- a/res/values-bg/strings.xml +++ b/res/values-bg/strings.xml @@ -73,7 +73,6 @@ Дисплей Експерт Търсене на приложения - Вид на синхронизация на базата данни Съвместимост на приложенията Root достъп Игнорирай сензорния екран diff --git a/res/values-ca/array.xml b/res/values-ca/array.xml index 4ea57069f..01d48006c 100644 --- a/res/values-ca/array.xml +++ b/res/values-ca/array.xml @@ -11,9 +11,4 @@ Fosc Clar - - Desactivat (no segur) - Normal - Complet - diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index ad655582c..5f19b0c64 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -84,7 +84,6 @@ La voleu actualitzar? Pantalla Usuari expert Cerca aplicacions - Mode de sincronització de la base de dades Compatibilitat de les aplicacions Versions incompatibles Root diff --git a/res/values-de/array.xml b/res/values-de/array.xml index cd7204197..722843fd8 100644 --- a/res/values-de/array.xml +++ b/res/values-de/array.xml @@ -11,9 +11,4 @@ Dunkel Hell - - Aus (unsicher) - Normal - Vollständig - diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 6dc755036..fc92f4380 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -106,7 +106,6 @@ Sollen diese aktualisiert werden? Zusätzliche Informationen anzeigen und zusätzliche Einstellungen aktivieren Experteneinstellungen ausblenden Anwendungen suchen - Datenbanksynchronisierungsart Kompatibilität der Anwendung Inkompatible Versionen Mit diesem Gerät inkompatible Anwendungsversionen anzeigen @@ -142,7 +141,6 @@ Sollen diese aktualisiert werden? Nicht signiert Adresse Anwendungsanzahl - Signatur Beschreibung Letzte Aktualisierung Aktualisierung diff --git a/res/values-el/array.xml b/res/values-el/array.xml index 27b24978d..2f02e1184 100644 --- a/res/values-el/array.xml +++ b/res/values-el/array.xml @@ -11,9 +11,4 @@ Σκοτεινό Φωτεινό - - Απενεργοποίηση (επισφαλής) - Κανονικό - Ολόκληρο - diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index e059b588e..e433b21dc 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -82,7 +82,6 @@ Εμφάνιση Για Προχωρημένους Αναζήτηση εφαρμογών - Λειτουργία συγχρονισμόυ της βάσης δεδομένων Συμβατότητα εφαμοργής Μη συμβατές εκδόσεις Root diff --git a/res/values-eo/array.xml b/res/values-eo/array.xml index fface9219..79d432e35 100644 --- a/res/values-eo/array.xml +++ b/res/values-eo/array.xml @@ -11,9 +11,4 @@ Dark Light - - Off (unsafe) - Normal - Full - diff --git a/res/values-es/array.xml b/res/values-es/array.xml index ce0b75b74..49d24bead 100644 --- a/res/values-es/array.xml +++ b/res/values-es/array.xml @@ -11,9 +11,4 @@ Oscuro Claro - - Desactivado (peligroso) - Normal - Completo - diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 2e97bfe73..d2d81ee93 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -105,7 +105,6 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Mostrar info extra y habilitar los ajustes extra Ocultar extras para usuarios experimentados Buscar aplicaciones - Modo síncrono de base de datos Compatibilidad de aplicaciones Versiones incompatibles Mostrar las versiones de aplicaciones incompatibles con el dispositivo @@ -141,7 +140,6 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo No firmado URL Número de aplicaciones - Firma Descripción Última actualización Actualizar diff --git a/res/values-eu/array.xml b/res/values-eu/array.xml index 80e6b4251..975c0749d 100644 --- a/res/values-eu/array.xml +++ b/res/values-eu/array.xml @@ -11,9 +11,4 @@ Dark Light - - Itzalita (ez da segurua) - Normala - Osoa - diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index 7c8e68156..18f56e589 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -62,7 +62,6 @@ Eguneratu nahi dituzu? Bistaratu Aditua Bilatu aplikazioak - Datu-base modu sinkronoa Aplikazioen bateragarritasuna Root Ezikusi egin ukipen-pantailari diff --git a/res/values-fa/array.xml b/res/values-fa/array.xml index 14d3a0421..f9d446d9a 100644 --- a/res/values-fa/array.xml +++ b/res/values-fa/array.xml @@ -11,9 +11,4 @@ تاریک روشن - - خاموش (ناامن) - عادی - کامل - diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml index e3b1a3c25..99a82996a 100644 --- a/res/values-fa/strings.xml +++ b/res/values-fa/strings.xml @@ -82,7 +82,6 @@ نمایش خارج‌سازی جستجوی برنامه‌ها - حالت هماهنگی پایگاه داده‌ها هماهنگی برنامه نسخه‌های غیرهماهنگ روت diff --git a/res/values-fi/array.xml b/res/values-fi/array.xml index d4ef0d648..24139afd6 100644 --- a/res/values-fi/array.xml +++ b/res/values-fi/array.xml @@ -11,9 +11,4 @@ Tumma Valo - - Pois päältä (vaarallinen) - Normaali - Täysi - diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml index 851070227..176dc9554 100644 --- a/res/values-fi/strings.xml +++ b/res/values-fi/strings.xml @@ -61,7 +61,6 @@ Tahdotko päivittää ne? Näyttö Asiantuntija Etsi sovelluksia - Tietokannan synkronointi-tila Sovellusten yhteensopivuus Yhteensopimattomat versiot Root diff --git a/res/values-fr/array.xml b/res/values-fr/array.xml index d204ebad8..61b0d885a 100644 --- a/res/values-fr/array.xml +++ b/res/values-fr/array.xml @@ -11,9 +11,4 @@ Sombre Clair - - Désactivé (non recommandé) - Normal - Complet - diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 81c0a7b8c..006f35037 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -82,7 +82,6 @@ Voulez-vous les mettre à jour ? Affichage Expert Rechercher des applications - Mode de synchronisation à la base de données Compatibilité de l\'application Versions incompatibles Root diff --git a/res/values-gl/array.xml b/res/values-gl/array.xml index 1770571ed..722add613 100644 --- a/res/values-gl/array.xml +++ b/res/values-gl/array.xml @@ -11,9 +11,4 @@ Escuro Claro - - Apagado (inseguro) - Normal - Completo - diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml index eb8eb5551..956b54a86 100644 --- a/res/values-gl/strings.xml +++ b/res/values-gl/strings.xml @@ -86,7 +86,6 @@ Quere actualizalos? Amosar Experto Buscar aplicativos - Modo de sincronización da base de datos Compatibilidade de aplicativos Versións incompatíbeis Root diff --git a/res/values-gu/array.xml b/res/values-gu/array.xml index 42eac981d..da5e2aa16 100644 --- a/res/values-gu/array.xml +++ b/res/values-gu/array.xml @@ -11,9 +11,4 @@ Dark Light - - બંધ (અસુરક્ષિત) - સામાન્ય - પૂર્ણ - diff --git a/res/values-it/array.xml b/res/values-it/array.xml index 7c2f1ef01..2eb94a3d6 100644 --- a/res/values-it/array.xml +++ b/res/values-it/array.xml @@ -11,9 +11,4 @@ Scuro Chiaro - - Disabilitato (non sicuro) - Normale - Completo - diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 1d32fb822..1df65c43f 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -105,7 +105,6 @@ Vuoi aggiornarlo? Mostra le informazioni ulteriori e abilita le impostazioni avanzate Nascondi le opzioni avanzate per utenti esperti Ricerca applicazioni - Modalità di sincronizzazione database Compatibilità applicazioni Versioni incompatibili Mostra le versioni incompatibili con il dispositivo @@ -141,7 +140,6 @@ Vuoi aggiornarlo? Non firmato URL Numero di applicazioni - Firma Descrizione Ultimo aggiornamento Aggiorna diff --git a/res/values-ko/array.xml b/res/values-ko/array.xml index 2bf47cb19..d00633ed0 100644 --- a/res/values-ko/array.xml +++ b/res/values-ko/array.xml @@ -11,9 +11,4 @@ 어두운 밝은 - - 해제 (안전하지 않음) - 보통 - 전체 - diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index 59536b009..d591f60f4 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -70,7 +70,6 @@ 표시 전문가 응용 프로그램 검색 - 데이터베이스 동기화 모드 응용 프로그램 호환성 호환되지 않는 버전 터치스크린 무시 diff --git a/res/values-nb/array.xml b/res/values-nb/array.xml index 2fb6485b1..bdf6445f2 100644 --- a/res/values-nb/array.xml +++ b/res/values-nb/array.xml @@ -11,9 +11,4 @@ Mørk Lys - - Av (utrygt) - Normalt - Fullt - diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index d8309655f..df3849b0a 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -77,7 +77,6 @@ Lisensiert GNU GPLv3. Vis Ekspert Søk i programliste - Modus for databasesynkronisering Programstøtte Ukompatible versjoner Rot diff --git a/res/values-nl/array.xml b/res/values-nl/array.xml index 19c7f0690..765ae052b 100644 --- a/res/values-nl/array.xml +++ b/res/values-nl/array.xml @@ -11,9 +11,4 @@ Donker Licht - - Uit (onveilig) - Normaal - Vol - diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index c03571660..2bd763c05 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -90,7 +90,6 @@ Wilt u ze vernieuwen? Laat zien Expert Zoek applicaties - Database sync-modus Applicatie verenigbaarheid Onverenigbare versies Root diff --git a/res/values-pl/array.xml b/res/values-pl/array.xml index 103711e39..e6076d5e5 100644 --- a/res/values-pl/array.xml +++ b/res/values-pl/array.xml @@ -11,9 +11,4 @@ Ciemny Jasny - - Wyłączone (niebezpieczne) - Normalny - Pełny - diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 121c09c6a..98186da07 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -105,7 +105,6 @@ Czy chcesz je zaktualizować? Pokaż dodatkowe informacje i włącz dodatkowe ustawienia Ukryj dodatki dla zaawansowanych użytkowników Wyszukaj aplikacje - Tryb synchronizacji bazy danych Kompatybilność aplikacji Niekompatybilne wersje Pokaż wersje aplikacji niekompatybilne z urządzeniem @@ -137,7 +136,6 @@ Czy chcesz je zaktualizować? Niepodpisany URL Liczba aplikacji - Podpis Opis Ostatnia modyfikacja Aktualizacja diff --git a/res/values-pt-rBR/array.xml b/res/values-pt-rBR/array.xml index 6fc95972c..1614797d3 100644 --- a/res/values-pt-rBR/array.xml +++ b/res/values-pt-rBR/array.xml @@ -11,9 +11,4 @@ Escuro Claro - - Desligada (inseguro) - Normal - Completa - diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index fa3159c33..acb4e94fe 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -82,7 +82,6 @@ Você deseja atualizá-los? Exibição Especialista Pesquisar aplicativos - Modo de sincronia do banco de dados Compatibilidade de aplicativo Versões incompatíveis Root diff --git a/res/values-ro/array.xml b/res/values-ro/array.xml index d8e58ad04..8c1fabb4e 100644 --- a/res/values-ro/array.xml +++ b/res/values-ro/array.xml @@ -11,9 +11,4 @@ Dark Light - - Inchis (nerecomandat) - Normal - Complet - diff --git a/res/values-ru/array.xml b/res/values-ru/array.xml index 13a7defd0..8b39f1618 100644 --- a/res/values-ru/array.xml +++ b/res/values-ru/array.xml @@ -11,9 +11,4 @@ Dark Light - - Откл. (опасно) - Обычный - Полный - diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index face682fe..4e1bd0b26 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -105,7 +105,6 @@ Показать дополнительную информацию и включить дополнительные настройки Скрыть дополнительные возможности для опытных пользователей Найти приложения - Режим синхронизации базы Совместимость приложений Несовместимые версии Показать версии программ, несовместимые с устройством @@ -141,7 +140,6 @@ Неподписанный URL Количество приложений - Подпись Описание Последние обновление Обновить diff --git a/res/values-sl/array.xml b/res/values-sl/array.xml index ba22674f8..bc3404374 100644 --- a/res/values-sl/array.xml +++ b/res/values-sl/array.xml @@ -11,9 +11,4 @@ Dark Light - - Izključeno (ni varno) - Običajno - Polno - diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index 73ce9a5ed..c50bdbc5c 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -52,7 +52,6 @@ Ga želite posodobiti? Tämä ohjelma sisältää mainontaa Napredno Iskanje aplikacij - Način sinhronizacije baze podatkov Združljivost aplikacij Yhteensopimattomat versiot Skrbnik diff --git a/res/values-sr/array.xml b/res/values-sr/array.xml index 0a0c851a6..7d414c084 100644 --- a/res/values-sr/array.xml +++ b/res/values-sr/array.xml @@ -11,9 +11,4 @@ Dark Light - - Искључено (није безбедно) - Нормално - Пуно - diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index b4fa35243..c0a94092c 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -81,7 +81,6 @@ Прикажи Стручни Претрага апликација - Режим синхронизације базе података Компатибилност апликације Рут Игнориши Додирни Екран diff --git a/res/values-sv/array.xml b/res/values-sv/array.xml index 9edfdb38c..12b70f0e9 100644 --- a/res/values-sv/array.xml +++ b/res/values-sv/array.xml @@ -11,9 +11,4 @@ Mörk Ljus - - Av (osäkert) - Normal - Fullständig - diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index 6cf446b9c..3f6b0fc61 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -105,7 +105,6 @@ Vill du uppdatera dem? Visa extra info och aktivera extra inställningar Dölj extraval för vana användare Sök program - Databassynkroniseringsläge Programkompatibilitet Inkompatibla versioner Visa appversioner som är inkompatibla med enheten @@ -141,7 +140,6 @@ Vill du uppdatera dem? Osignerad URL Antal appar - Signatur Beskrivning Senaste uppdatering Uppdatera diff --git a/res/values-tr/array.xml b/res/values-tr/array.xml index b7840e71e..73ba4a61a 100644 --- a/res/values-tr/array.xml +++ b/res/values-tr/array.xml @@ -11,9 +11,4 @@ Koyu Açık - - Devre dışı (güvenli değildir) - Normal - Tümü - diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml index 746a0b890..27333f770 100644 --- a/res/values-tr/strings.xml +++ b/res/values-tr/strings.xml @@ -82,7 +82,6 @@ Güncellemek ister misiniz? Görüntüleme Uzman Uygulama ara - Veritabanı eşleşme modu Uygulama uyumu Uyumsuz sürümler Root diff --git a/res/values-ug/array.xml b/res/values-ug/array.xml index 35b79432f..25e167824 100644 --- a/res/values-ug/array.xml +++ b/res/values-ug/array.xml @@ -11,9 +11,4 @@ قاراڭغۇ يورۇق - - تاقاق (بىخەتەر ئەمەس) - نورمال - تولۇق - diff --git a/res/values-ug/strings.xml b/res/values-ug/strings.xml index ffb278803..53c8c774b 100644 --- a/res/values-ug/strings.xml +++ b/res/values-ug/strings.xml @@ -82,7 +82,6 @@ كۆرسەت ئالىي ئەپ ئىزدە - ساندان قەدەمداش ھالەت ئەپ ماسلىشىشچانلىقى ماسلاشمايدىغان نەشرىلىرى Root diff --git a/res/values-uk/array.xml b/res/values-uk/array.xml index 0d43d54c8..83ea4a1e8 100644 --- a/res/values-uk/array.xml +++ b/res/values-uk/array.xml @@ -11,9 +11,4 @@ Dark Light - - Ніколи (небезпечно) - Типово - Повністю - diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index 15ed67ee2..478059d47 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -53,7 +53,6 @@ Звантаження скасовано Експерт Пошук програм - Синхронізація БД Сумісність Суперкористувач Ігнорувати тачскрін diff --git a/res/values-zh-rCN/array.xml b/res/values-zh-rCN/array.xml index 120aefcf0..dd10ac48a 100644 --- a/res/values-zh-rCN/array.xml +++ b/res/values-zh-rCN/array.xml @@ -11,9 +11,4 @@ Dark Light - - 关闭(存在安全风险) - 正常 - 完整的 - diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index 80451bdda..b12da2de2 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -53,7 +53,6 @@ 下载取消 高级 搜索应用 - 数据同步模式 应用兼容性 Root 忽略需要触屏的应用 From 91363bf753e597d10b9b64d261e6417bbe3789a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 5 Mar 2014 22:58:58 +0100 Subject: [PATCH 167/282] Re-run fix-ellipsis --- res/values-bg/strings.xml | 2 +- res/values-ca/strings.xml | 4 ++-- res/values-el/strings.xml | 4 ++-- res/values-es/strings.xml | 4 ++-- res/values-eu/strings.xml | 4 ++-- res/values-fa/strings.xml | 4 ++-- res/values-fi/strings.xml | 4 ++-- res/values-fr/strings.xml | 2 +- res/values-gl/strings.xml | 2 +- res/values-it/strings.xml | 4 ++-- res/values-ko/strings.xml | 2 +- res/values-nb/strings.xml | 4 ++-- res/values-nl/strings.xml | 4 ++-- res/values-pl/strings.xml | 4 ++-- res/values-pt-rBR/strings.xml | 4 ++-- res/values-ro/strings.xml | 4 ++-- res/values-ru/strings.xml | 4 ++-- res/values-sl/strings.xml | 2 +- res/values-sr/strings.xml | 4 ++-- res/values-sv/strings.xml | 2 +- res/values-tr/strings.xml | 2 +- res/values-uk/strings.xml | 2 +- 22 files changed, 36 insertions(+), 36 deletions(-) diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml index c49dd2e83..de283c920 100644 --- a/res/values-bg/strings.xml +++ b/res/values-bg/strings.xml @@ -46,7 +46,7 @@ %d налични актуализации. Актуализации на F-Droid са налични Моля изчакай - Обновявани на списъка с приложения... + Обновявани на списъка с приложения… Взимане на приложението от Адрес на хранилището Списъкът на хранилищата е променен. diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index 5f19b0c64..eed37b7a4 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -48,7 +48,7 @@ L\'adreça d\'un dipòsit té un aspecte com ara: http://f-droid.org/repoHi ha %d actualitzacions disponibles. Hi ha actualitzacions de l\'F-Droid disponibles Un moment si us plau - S\'està actualitzant la llista d\'aplicacions... + S\'està actualitzant la llista d\'aplicacions… S\'està obtenint l\'aplicació des de Adreça del dipòsit Aquest repositori ja existeix. @@ -99,7 +99,7 @@ La voleu actualitzar? %1$s S\'està connectant a %1$s - S\'està comprovant la compatibilitat de les aplicacions amb el vostre dispositiu... + S\'està comprovant la compatibilitat de les aplicacions amb el vostre dispositiu… No es fa servir cap permís. Permisos de la versió %s Mostra els permisos diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index e433b21dc..6a2b5e8f7 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -47,7 +47,7 @@ %d διαθέσιμες ενημερώσεις. Διαθέσιμες ενημερώσεις για το F-Droid Παρακαλώ περιμένετε - Ενημέρωση λίστα εφαρμογών... + Ενημέρωση λίστα εφαρμογών… Λήψη εφαρμογών από Διεύθυνση αποθετηρίου Η λίστα με τα χρησιμοποιούμενα αποθετήρια έχει αλλάξει. @@ -97,7 +97,7 @@ %1$s Σύνδεση με %1$s - Έλεγχος συμβατότητας εφαρμογών με τη συσκευή σας... + Έλεγχος συμβατότητας εφαρμογών με τη συσκευή σας… Δεν χρησιμοποιείται καμία άδεια. Άδειες για την έκδοση %s Εμφάνιση αδειών diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index d2d81ee93..03e583dfe 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -61,7 +61,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo %d actualizaciones disponibles. Actualizaciones de F-Droid disponibles Por favor, espera - Actualizando la lista de aplicaciones... + Actualizando la lista de aplicaciones… Obteniendo la aplicación de Dirección del repositorio Huella digital (opcional) @@ -126,7 +126,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo %1$s Conectando a %1$s - Comprobando la compatibilidad de las aplicaciones con tu dispositivo... + Comprobando la compatibilidad de las aplicaciones con tu dispositivo… No se usan permisos. Permisos para la versión %s Mostrar permisos diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index 18f56e589..f37301049 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -35,7 +35,7 @@ GNU GPLv3 lizentziapean argitaratua. %d eguneraketa eskuragarri. F-Droid eguneraketak eskuragarri Mesedez itxaron - Aplikazio-zerrenda eguneratzen... + Aplikazio-zerrenda eguneratzen… Aplikazioa eskuratzen hemendik Biltegiaren helbidea Erabilitako biltegien zerrenda aldatu egin da. @@ -70,7 +70,7 @@ Eguneratu nahi dituzu? Azkenaldian eguneratua %1$s(e)ra konektatzen - Aplikazioak zure gailuarekin bateragarriak diren egiaztatzen... + Aplikazioak zure gailuarekin bateragarriak diren egiaztatzen… Ez da baimenik erabiltzen. %s bertsioarentzako baimenak Erakutsi baimenak diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml index 99a82996a..9a6fa1356 100644 --- a/res/values-fa/strings.xml +++ b/res/values-fa/strings.xml @@ -47,7 +47,7 @@ %d به‌روزرسانی موجود است. به‌روزرسانی‌های F-Droid موجود هستند لطفاً صبر کنید - به‌روزرسانی فهرست برنامه‌ها... + به‌روزرسانی فهرست برنامه‌ها… گرفتن برنامه از نشانی مخزن فهرست مخزن‌ها تغییر یافته‌است. @@ -97,7 +97,7 @@ %1$s اتصال به %1$s - بررسی سازگاری برنامه‌ها با دستگاه شما... + بررسی سازگاری برنامه‌ها با دستگاه شما… دسترسی‌ای استفاده نشده‌است. دسترسی‌های نسخهٔ %s نمایش دسترسی‌ها diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml index 176dc9554..26c27f90d 100644 --- a/res/values-fi/strings.xml +++ b/res/values-fi/strings.xml @@ -33,7 +33,7 @@ %d päivitystä saatavilla. F-Droid: Päivityksiä saatavilla Odota hetki - Päivitetään sovelluslistaa... + Päivitetään sovelluslistaa… Haetaan sovellusta lähteestä Sovelluslähteen osoite Lista käytetyistä sovelluslähteistä on muuttumut. @@ -68,6 +68,6 @@ Tahdotko päivittää ne? Kaikki Uutta Viimeaikoina päivitetty - Tarkistetaan ohjelman yhteensopivuutta laitteesi kanssa... + Tarkistetaan ohjelman yhteensopivuutta laitteesi kanssa… Teema diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 006f35037..51677523d 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -47,7 +47,7 @@ L\'URL d\'un dépôt ressemble à ceci : http://f-droid.org/repo %d mises à jour sont disponibles. Des mises à jour F-Droid sont disponibles Patientez - Mise à jour de la liste d\'applications... + Mise à jour de la liste d\'applications… Réception d\'application de Adresse du dépôt La liste des dépôts utilisés a changé. diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml index 956b54a86..0175f1829 100644 --- a/res/values-gl/strings.xml +++ b/res/values-gl/strings.xml @@ -51,7 +51,7 @@ Un enderezo a un repositorio sería algo %d actualizacións dispoñíbeis Actualizacións de F-Droid dispoñíbeis Agarde por favor - Actualizando a lista de aplicativos... + Actualizando a lista de aplicativos… Obtención do aplicativo desde Enderezo do repositorio Cambiou a lista de repositorios usados. diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 1df65c43f..20fa7ae38 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -61,7 +61,7 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo %d aggiornamenti disponibili. Aggiornamenti per F-Droid Disponibili Attendere prego - Aggiornamento elenco applicazioni... + Aggiornamento elenco applicazioni… Scaricamento applicazione da Indirizzo repository Impronta digitale (opzionale) @@ -126,7 +126,7 @@ Vuoi aggiornarlo? %1$s Connessione a %1$s - Controllo compatibilità applicazioni con il tuo dispositivo... + Controllo compatibilità applicazioni con il tuo dispositivo… Non viene usata alcuna autorizzazione. Autorizzazioni per la versione %s Mostra autorizzazioni diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index d591f60f4..5baeb291d 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -39,7 +39,7 @@ %d개의 업데이트를 사용할 수 있습니다. F-Droid 업데이트를 사용할 수 있습니다. 잠시만 기다려주세요 - 응용 프로그램 목록 업데이트중... + 응용 프로그램 목록 업데이트중… 에서 응용프로그램 가져오기 저장소 주소 사용된 저장소의 목록이 변경되었습니다. diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index df3849b0a..02159393f 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -43,7 +43,7 @@ Lisensiert GNU GPLv3. %d oppdateringer tilgjengelig. F-Droid: Oppdateringer tilgjengelig Vennligst vent - Oppdaterer applikasjonsliste... + Oppdaterer applikasjonsliste… Henter program fra Registeradresse Listen over brukte register har endret seg. Vil du oppdatere dem? @@ -92,7 +92,7 @@ Lisensiert GNU GPLv3. %1$s Kobler til %1$s - Sjekker programstøtte for ditt utstyr... + Sjekker programstøtte for ditt utstyr… Krever ingen tillatelser. Tillatelser for versjon %s Vis tillatelser diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index 2bd763c05..08660f2a0 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -55,7 +55,7 @@ Een bron-adres ziet er ongeveer %d updates beschikbaar F-Droid update beschikbaar Even geduld aub - Applicatie-lijst vernieuwen... + Applicatie-lijst vernieuwen… downloaden applicatie van Bron-adres De lijst van gebruikte bronnen is veranderd. @@ -104,7 +104,7 @@ Wilt u ze vernieuwen? %2$d van %3$d van %1$s Verbinden met %1$s - Controleer app compatibiliteit met uw apparaat... + Controleer app compatibiliteit met uw apparaat… Geen permissies worden gebruikt Permissies voor versie %s Laat permissies zien diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 98186da07..fc1cc4a16 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -61,7 +61,7 @@ Przykładowy adres repozytorium: https://f-droid.org/repo Dostępnych jest %d uaktualnień Uaktualnienie F-Droid jest dostępne Proszę czekać - Aktualizowanie listy aplikacji... + Aktualizowanie listy aplikacji… Pobieranie aplikacji z Adres repozytorium Odcisk palca (opcjonalnie) @@ -122,7 +122,7 @@ Czy chcesz je zaktualizować? Przetwarzanie aplikacji %2$d / %3$d z %1$s Trwa łączenie z %1$s - Sprawdzanie kompatybilności aplikacji z urządzeniem... + Sprawdzanie kompatybilności aplikacji z urządzeniem… Brak użytych uprawnień. Uprawnienia dla wersji %s Wyświetl uprawnienia diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index acb4e94fe..6c3b972bc 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -47,7 +47,7 @@ Um endereço do repositório é algo similar a isto: http://f-droid.org/repo%d atualizações disponíveis. Atualizações do F-Droid Disponíveis Aguarde - Atualizando a lista de aplicativos... + Atualizando a lista de aplicativos… Baixando aplicativo de Endereço do repositório A lista de repositórios usados mudou. @@ -97,7 +97,7 @@ Você deseja atualizá-los? %1$s Conectando-se a %1$s - Verificando compatibilidade de aplicativos com o seu dispositivo... + Verificando compatibilidade de aplicativos com o seu dispositivo… Nenhuma permissão utilizada. Permissões para a versão %s Mostrar permissões diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml index 8a2bc508f..40c1a4fe3 100644 --- a/res/values-ro/strings.xml +++ b/res/values-ro/strings.xml @@ -25,6 +25,6 @@ Distribuit sub licenta GNU GPLv3. Actualizare depozit aplicatii Disponibil Actualizare - Asteptati ... - Se actualizeaza lista ... + Asteptati … + Se actualizeaza lista … diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 4e1bd0b26..6bd3efce8 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -61,7 +61,7 @@ Обновлений доступно - %d. Доступны обновления для F-Droid Подождите - Список приложений обновляется... + Список приложений обновляется… Взять приложение из Адрес репозитория Отпечаток ключа (опционально) @@ -126,7 +126,7 @@ %1$s Соединение с %1$s - Проверка совместимости приложений с устройством... + Проверка совместимости приложений с устройством… Разрешений не требуется. Разрешения для версии %s Показывать разрешения diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index c50bdbc5c..8f98a56bb 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -28,7 +28,7 @@ Izdan z licenco GNU GPLv3. Na razpolago Posodobitve Počakajte prosim - Poteka posodobitev spiska aplikacij ... + Poteka posodobitev spiska aplikacij … Prejem aplikacije iz Naslov skladišča Spisek uporabljenih skladišč se je spremenil. diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index c0a94092c..fad6b6304 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -47,7 +47,7 @@ %d нове/нових верзија на располагању Ажурирање Ф-Дроида на располагању. Сачекајте - Ажурира се листа апликација... + Ажурира се листа апликација… Скида се апликација са Адреса ризнице Промењена је листа ризница у употреби. @@ -95,7 +95,7 @@ %1$s Повезивање са %1$s - Проверава се да ли је апликација компатибилна са вашим уређајем... + Проверава се да ли је апликација компатибилна са вашим уређајем… Не захтевају се никакве дозволе. Дозволе за верзију %s Прикажи дозволе diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index 3f6b0fc61..eb3143a9a 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -61,7 +61,7 @@ En förrådsadress ser ut så här: https://f-droid.org/repo %d uppdateringar finns tillgängliga. Uppdateringar för F-Droid tillgängliga Var vänlig vänta - Uppdaterar programlistan... + Uppdaterar programlistan… Hämtar program från Förrådsadress fingeravtryck (valfritt) diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml index 27333f770..28aaf249d 100644 --- a/res/values-tr/strings.xml +++ b/res/values-tr/strings.xml @@ -47,7 +47,7 @@ Bir depo adresi şuna benzer: https://f-droid.org/repo %d güncelleme bulunmaktadır. F-Droid güncellemeleri bulunmaktadır Bekleyiniz - Uygulama listesi güncelleniyor... + Uygulama listesi güncelleniyor… Uygulama buradan alınıyor: Depo adresi Kullanılan depoların listesi değişti. diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index 478059d47..d9af9a175 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -28,7 +28,7 @@ Наявне Оновлення Зачекайте - Оновлюю список програм... + Оновлюю список програм… Звантажую програму Адреса репозиторію Список репозиторіїв змінено. From 68f266d6bee75974798c10cded0bfc53dac57353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 5 Mar 2014 23:12:55 +0100 Subject: [PATCH 168/282] Remove silly values-qqq --- res/values-qqq/strings.xml | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 res/values-qqq/strings.xml diff --git a/res/values-qqq/strings.xml b/res/values-qqq/strings.xml deleted file mode 100644 index 0b943988a..000000000 --- a/res/values-qqq/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - »Alle Aktualisierungen ignorieren« wird nicht richtig dargestellt. Sowohl horizontal als auch vertikal. Deshalb abgekürzte Wörter. Etwa 23 Zeichen stehen zur Verfügung. Ak·tu·a·li·sie·ren, ig·no·rie·ren - »Alle Aktualisierungen ignorieren« wird nicht richtig dargestellt. Sowohl horizontal als auch vertikal. Deshalb abgekürzte Wörter. Etwa 23 Zeichen stehen zur Verfügung. Ak·tu·a·li·sie·rung, ig·no·rie·ren - From 81fcd44b66a83b3889bcd75e3cd92f76d457b6f5 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 6 Mar 2014 08:05:51 +1100 Subject: [PATCH 169/282] Fixed update notification count The update notification was not taking ignored apps into account. This is the first manifestation of a class of bug I feared whereby the properties of an App object are not initialized, but no error is thrown. It occured because we were iterating over apps that were created from the index file, rather than our database. Hence, they had no knowledge about whether they should be ignored or not. Also took the chance to perform a minor refactor of UpdateService class. The onHandleIntent method was getting huge, so I extracted two methods: verifyIsTimeForScheduledRun() and performUpdateNotification(), as well as removing the unused "success" flag. The two methods should theoretically make the class more testable later, as we can test the scheduled run code, and the update notification code in separate tests, but we'll wait and see if that eventuates. --- src/org/fdroid/fdroid/UpdateService.java | 147 +++++++++++--------- src/org/fdroid/fdroid/data/AppProvider.java | 28 +++- 2 files changed, 110 insertions(+), 65 deletions(-) diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index 5ff446905..f22b93281 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -201,6 +201,45 @@ public class UpdateService extends IntentService implements ProgressListener { return receiver == null; } + /** + * Check whether it is time to run the scheduled update. + * We don't want to run if: + * - The time between scheduled runs is set to zero (though don't know + * when that would occur) + * - Last update was too recent + * - Not on wifi, but the property for "Only auto update on wifi" is set. + * @return True if we are due for a scheduled update. + */ + private boolean verifyIsTimeForScheduledRun() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); + long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0); + String sint = prefs.getString(Preferences.PREF_UPD_INTERVAL, "0"); + int interval = Integer.parseInt(sint); + if (interval == 0) { + Log.d("FDroid", "Skipping update - disabled"); + return false; + } + long elapsed = System.currentTimeMillis() - lastUpdate; + if (elapsed < interval * 60 * 60 * 1000) { + Log.d("FDroid", "Skipping update - done " + elapsed + + "ms ago, interval is " + interval + " hours"); + return false; + } + + // If we are to update the repos only on wifi, make sure that + // connection is active + if (prefs.getBoolean(Preferences.PREF_UPD_WIFI_ONLY, false)) { + ConnectivityManager conMan = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo.State wifi = conMan.getNetworkInfo(1).getState(); + if (wifi != NetworkInfo.State.CONNECTED && + wifi != NetworkInfo.State.CONNECTING) { + Log.d("FDroid", "Skipping update - wifi not available"); + return false; + } + } + return true; + } + @Override protected void onHandleIntent(Intent intent) { @@ -210,39 +249,13 @@ public class UpdateService extends IntentService implements ProgressListener { long startTime = System.currentTimeMillis(); String errmsg = ""; try { - - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(getBaseContext()); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); // See if it's time to actually do anything yet... - if (isScheduledRun()) { - long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0); - String sint = prefs.getString(Preferences.PREF_UPD_INTERVAL, "0"); - int interval = Integer.parseInt(sint); - if (interval == 0) { - Log.d("FDroid", "Skipping update - disabled"); - return; - } - long elapsed = System.currentTimeMillis() - lastUpdate; - if (elapsed < interval * 60 * 60 * 1000) { - Log.d("FDroid", "Skipping update - done " + elapsed - + "ms ago, interval is " + interval + " hours"); - return; - } - - // If we are to update the repos only on wifi, make sure that - // connection is active - if (prefs.getBoolean(Preferences.PREF_UPD_WIFI_ONLY, false)) { - ConnectivityManager conMan = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo.State wifi = conMan.getNetworkInfo(1).getState(); - if (wifi != NetworkInfo.State.CONNECTED && - wifi != NetworkInfo.State.CONNECTING) { - Log.d("FDroid", "Skipping update - wifi not available"); - return; - } - } - } else { + if (!isScheduledRun()) { Log.d("FDroid", "Unscheduled (manually requested) update"); + } else if (!verifyIsTimeForScheduledRun()) { + return; } // Grab some preliminary information, then we can release the @@ -255,7 +268,6 @@ public class UpdateService extends IntentService implements ProgressListener { List unchangedRepos = new ArrayList(); List updatedRepos = new ArrayList(); List disabledRepos = new ArrayList(); - boolean success = true; boolean changes = false; for (Repo repo : repos) { @@ -289,14 +301,10 @@ 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."); - } else if (changes && success) { - - sendStatus(STATUS_INFO, - getString(R.string.status_checking_compatibility)); + if (!changes) { + Log.d("FDroid", "Not checking app details or compatibility, ecause all repos were up to date."); + } else { + sendStatus(STATUS_INFO, getString(R.string.status_checking_compatibility)); List listOfAppsToUpdate = new ArrayList(); listOfAppsToUpdate.addAll(appsToUpdate.values()); @@ -312,34 +320,19 @@ public class UpdateService extends IntentService implements ProgressListener { removeApksNoLongerInRepo(listOfAppsToUpdate, updatedRepos); removeAppsWithoutApks(); notifyContentProviders(); - } - if (success && changes && prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, false)) { - int updateCount = 0; - for (App app : appsToUpdate.values()) { - if (app.canAndWantToUpdate(this)) { - updateCount++; - } - } - - if (updateCount > 0) { - showAppUpdatesNotification(updateCount); + if (prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, false)) { + performUpdateNotification(appsToUpdate.values()); } } - if (!success) { - if (errmsg.length() == 0) - errmsg = "Unknown error"; - sendStatus(STATUS_ERROR, errmsg); + Editor e = prefs.edit(); + e.putLong(Preferences.PREF_UPD_LAST, System.currentTimeMillis()); + e.commit(); + if (changes) { + sendStatus(STATUS_COMPLETE_WITH_CHANGES); } else { - Editor e = prefs.edit(); - e.putLong(Preferences.PREF_UPD_LAST, System.currentTimeMillis()); - e.commit(); - if (changes) { - sendStatus(STATUS_COMPLETE_WITH_CHANGES); - } else { - sendStatus(STATUS_COMPLETE_AND_SAME); - } + sendStatus(STATUS_COMPLETE_AND_SAME); } } catch (Exception e) { @@ -464,7 +457,35 @@ public class UpdateService extends IntentService implements ProgressListener { } } - private void showAppUpdatesNotification(int updates) throws Exception { + private void performUpdateNotification(Collection apps) { + int updateCount = 0; + + // This may be somewhat strange, because we usually would just trust + // App.canAndWantToUpdate(). The only problem is that the "appsToUpdate" + // list only contains data from the repo index, not our database. + // As such, it doesn't know if we want to ignore the apps or not. For that, we + // need to query the database manually and identify those which are to be ignored. + String[] projection = { AppProvider.DataColumns.APP_ID }; + List appsToIgnore = AppProvider.Helper.findIgnored(this, projection); + for (App app : apps) { + boolean ignored = false; + for(App appIgnored : appsToIgnore) { + if (appIgnored.id.equals(app.id)) { + ignored = true; + break; + } + } + if (!ignored && app.hasUpdates(this)) { + updateCount++; + } + } + + if (updateCount > 0) { + showAppUpdatesNotification(updateCount); + } + } + + private void showAppUpdatesNotification(int updates) { Log.d("FDroid", "Notifying " + updates + " updates."); NotificationCompat.Builder builder = new NotificationCompat.Builder(this) diff --git a/src/org/fdroid/fdroid/data/AppProvider.java b/src/org/fdroid/fdroid/data/AppProvider.java index 9475cfb62..76d0121fb 100644 --- a/src/org/fdroid/fdroid/data/AppProvider.java +++ b/src/org/fdroid/fdroid/data/AppProvider.java @@ -7,6 +7,7 @@ import android.net.Uri; import android.util.Log; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; import java.util.*; @@ -32,6 +33,12 @@ public class AppProvider extends FDroidProvider { return cursorToList(cursor); } + public static List findIgnored(Context context, String[] projection) { + Uri uri = AppProvider.getIgnoredUri(); + Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null); + return cursorToList(cursor); + } + private static List cursorToList(Cursor cursor) { List apps = new ArrayList(); if (cursor != null) { @@ -227,6 +234,7 @@ public class AppProvider extends FDroidProvider { private static final String PATH_RECENTLY_UPDATED = "recentlyUpdated"; private static final String PATH_NEWLY_ADDED = "newlyAdded"; private static final String PATH_CATEGORY = "category"; + private static final String PATH_IGNORED = "ignored"; private static final int CAN_UPDATE = CODE_SINGLE + 1; private static final int INSTALLED = CAN_UPDATE + 1; @@ -236,9 +244,11 @@ public class AppProvider extends FDroidProvider { private static final int RECENTLY_UPDATED = APPS + 1; private static final int NEWLY_ADDED = RECENTLY_UPDATED + 1; private static final int CATEGORY = NEWLY_ADDED + 1; + private static final int IGNORED = CATEGORY + 1; static { matcher.addURI(getAuthority(), null, CODE_LIST); + matcher.addURI(getAuthority(), PATH_IGNORED, IGNORED); matcher.addURI(getAuthority(), PATH_RECENTLY_UPDATED, RECENTLY_UPDATED); matcher.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED); matcher.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY); @@ -262,6 +272,10 @@ public class AppProvider extends FDroidProvider { return Uri.withAppendedPath(getContentUri(), PATH_NEWLY_ADDED); } + public static Uri getIgnoredUri() { + return Uri.withAppendedPath(getContentUri(), PATH_IGNORED); + } + public static Uri getCategoryUri(String category) { return getContentUri().buildUpon() .appendPath(PATH_CATEGORY) @@ -388,6 +402,12 @@ public class AppProvider extends FDroidProvider { return new QuerySelection(selection, args); } + private QuerySelection queryIgnored() { + String selection = "fdroid_app.ignoreAllUpdates = 1 OR " + + "fdroid_app.ignoreThisUpdate >= fdroid_app.suggestedVercode"; + return new QuerySelection(selection); + } + private QuerySelection queryNewlyAdded() { String selection = "fdroid_app.added > ?"; String[] args = { Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory()) }; @@ -459,6 +479,10 @@ public class AppProvider extends FDroidProvider { query = query.add(queryApps(uri.getLastPathSegment())); break; + case IGNORED: + query = query.add(queryIgnored()); + break; + case CATEGORY: query = query.add(queryCategory(uri.getLastPathSegment())); break; @@ -503,7 +527,7 @@ public class AppProvider extends FDroidProvider { break; default: - throw new UnsupportedOperationException("Can't delete yet"); + throw new UnsupportedOperationException("Delete not supported for " + uri + "."); } @@ -531,7 +555,7 @@ public class AppProvider extends FDroidProvider { break; default: - throw new UnsupportedOperationException("Update not supported for '" + uri + "'."); + throw new UnsupportedOperationException("Update not supported for " + uri + "."); } int count = write().update(getTableName(), values, query.getSelection(), query.getArgs()); From 9fd8da42a1f17695cd17058f71d2a0800f8e43e2 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 6 Mar 2014 15:59:20 +1100 Subject: [PATCH 170/282] Adding test coverage for "AppProvider.Helper.findIgnored()" Also added tests for canAndWantToUpdate() while I was at it. --- src/org/fdroid/fdroid/data/AppProvider.java | 9 -- .../org/fdroid/fdroid/AppProviderTest.java | 110 ++++++++++++++++++ test/src/org/fdroid/fdroid/TestUtils.java | 2 +- 3 files changed, 111 insertions(+), 10 deletions(-) diff --git a/src/org/fdroid/fdroid/data/AppProvider.java b/src/org/fdroid/fdroid/data/AppProvider.java index 76d0121fb..463b57317 100644 --- a/src/org/fdroid/fdroid/data/AppProvider.java +++ b/src/org/fdroid/fdroid/data/AppProvider.java @@ -137,23 +137,14 @@ public class AppProvider extends FDroidProvider { public static final String SUGGESTED_VERSION_CODE = "suggestedVercode"; public static final String UPSTREAM_VERSION = "upstreamVersion"; public static final String UPSTREAM_VERSION_CODE = "upstreamVercode"; - public static final String CURRENT_APK = null; public static final String ADDED = "added"; public static final String LAST_UPDATED = "lastUpdated"; - public static final String INSTALLED_VERSION = null; - public static final String INSTALLED_VERCODE = null; - public static final String USER_INSTALLED = null; public static final String CATEGORIES = "categories"; public static final String ANTI_FEATURES = "antiFeatures"; public static final String REQUIREMENTS = "requirements"; - public static final String FILTERED = null; - public static final String HAS_UPDATES = null; - public static final String TO_UPDATE = null; public static final String IGNORE_ALLUPDATES = "ignoreAllUpdates"; public static final String IGNORE_THISUPDATE = "ignoreThisUpdate"; public static final String ICON_URL = "iconUrl"; - public static final String UPDATED = null; - public static final String APKS = null; public interface SuggestedApk { public static final String VERSION = "suggestedApkVersion"; diff --git a/test/src/org/fdroid/fdroid/AppProviderTest.java b/test/src/org/fdroid/fdroid/AppProviderTest.java index 2768e858b..80e19bd72 100644 --- a/test/src/org/fdroid/fdroid/AppProviderTest.java +++ b/test/src/org/fdroid/fdroid/AppProviderTest.java @@ -1,9 +1,11 @@ package org.fdroid.fdroid; +import android.content.ContentResolver; import android.content.ContentValues; import android.database.Cursor; import mock.MockCategoryResources; +import mock.MockContextSwappableComponents; import mock.MockInstallablePackageManager; import org.fdroid.fdroid.data.ApkProvider; @@ -33,6 +35,10 @@ public class AppProviderTest extends FDroidProviderTest { }; } + public void testCantFindApp() { + assertNull(AppProvider.Helper.findById(getMockContentResolver(), "com.example.doesnt-exist")); + } + public void testUris() { assertInvalidUri(AppProvider.getAuthority()); assertInvalidUri(ApkProvider.getContentUri()); @@ -65,6 +71,110 @@ public class AppProviderTest extends FDroidProviderTest { } } + private void insertAndInstallApp( + MockInstallablePackageManager packageManager, + String id, int installedVercode, int suggestedVercode, + boolean ignoreAll, int ignoreVercode) { + ContentValues values = new ContentValues(3); + values.put(AppProvider.DataColumns.SUGGESTED_VERSION_CODE, suggestedVercode); + values.put(AppProvider.DataColumns.IGNORE_ALLUPDATES, ignoreAll); + values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreVercode); + insertApp(id, "App: " + id, values); + + packageManager.install(id, installedVercode, "v" + installedVercode); + } + + public void testCanUpdate() { + + MockContextSwappableComponents c = getSwappableContext(); + + MockInstallablePackageManager pm = new MockInstallablePackageManager(); + c.setPackageManager(pm); + + insertApp("not installed", "not installed"); + insertAndInstallApp(pm, "installed, only one version available", 1, 1, false, 0); + insertAndInstallApp(pm, "installed, already latest, no ignore", 10, 10, false, 0); + insertAndInstallApp(pm, "installed, already latest, ignore all", 10, 10, true, 0); + insertAndInstallApp(pm, "installed, already latest, ignore latest", 10, 10, false, 10); + insertAndInstallApp(pm, "installed, already latest, ignore old", 10, 10, false, 5); + insertAndInstallApp(pm, "installed, old version, no ignore", 5, 10, false, 0); + insertAndInstallApp(pm, "installed, old version, ignore all", 5, 10, true, 0); + insertAndInstallApp(pm, "installed, old version, ignore latest", 5, 10, false, 10); + insertAndInstallApp(pm, "installed, old version, ignore newer, but not latest", 5, 10, false, 8); + + ContentResolver r = getMockContentResolver(); + + // Can't "update", although can "install"... + App notInstalled = AppProvider.Helper.findById(r, "not installed"); + assertFalse(notInstalled.canAndWantToUpdate(c)); + + App installedOnlyOneVersionAvailable = AppProvider.Helper.findById(r, "installed, only one version available"); + App installedAlreadyLatestNoIgnore = AppProvider.Helper.findById(r, "installed, already latest, no ignore"); + App installedAlreadyLatestIgnoreAll = AppProvider.Helper.findById(r, "installed, already latest, ignore all"); + App installedAlreadyLatestIgnoreLatest = AppProvider.Helper.findById(r, "installed, already latest, ignore latest"); + App installedAlreadyLatestIgnoreOld = AppProvider.Helper.findById(r, "installed, already latest, ignore old"); + + assertFalse(installedOnlyOneVersionAvailable.canAndWantToUpdate(c)); + assertFalse(installedAlreadyLatestNoIgnore.canAndWantToUpdate(c)); + assertFalse(installedAlreadyLatestIgnoreAll.canAndWantToUpdate(c)); + assertFalse(installedAlreadyLatestIgnoreLatest.canAndWantToUpdate(c)); + assertFalse(installedAlreadyLatestIgnoreOld.canAndWantToUpdate(c)); + + App installedOldNoIgnore = AppProvider.Helper.findById(r, "installed, old version, no ignore"); + App installedOldIgnoreAll = AppProvider.Helper.findById(r, "installed, old version, ignore all"); + App installedOldIgnoreLatest = AppProvider.Helper.findById(r, "installed, old version, ignore latest"); + App installedOldIgnoreNewerNotLatest = AppProvider.Helper.findById(r, "installed, old version, ignore newer, but not latest"); + + assertTrue(installedOldNoIgnore.canAndWantToUpdate(c)); + assertFalse(installedOldIgnoreAll.canAndWantToUpdate(c)); + assertFalse(installedOldIgnoreLatest.canAndWantToUpdate(c)); + assertTrue(installedOldIgnoreNewerNotLatest.canAndWantToUpdate(c)); + } + + public void testIgnored() { + + MockInstallablePackageManager pm = new MockInstallablePackageManager(); + getSwappableContext().setPackageManager(pm); + + insertApp("not installed", "not installed"); + insertAndInstallApp(pm, "installed, only one version available", 1, 1, false, 0); + insertAndInstallApp(pm, "installed, already latest, no ignore", 10, 10, false, 0); + insertAndInstallApp(pm, "installed, already latest, ignore all", 10, 10, true, 0); + insertAndInstallApp(pm, "installed, already latest, ignore latest", 10, 10, false, 10); + insertAndInstallApp(pm, "installed, already latest, ignore old", 10, 10, false, 5); + insertAndInstallApp(pm, "installed, old version, no ignore", 5, 10, false, 0); + insertAndInstallApp(pm, "installed, old version, ignore all", 5, 10, true, 0); + insertAndInstallApp(pm, "installed, old version, ignore latest", 5, 10, false, 10); + insertAndInstallApp(pm, "installed, old version, ignore newer, but not latest", 5, 10, false, 8); + + assertResultCount(10, AppProvider.getContentUri()); + + String[] projection = { AppProvider.DataColumns.APP_ID }; + List ignoredApps = AppProvider.Helper.findIgnored(getMockContext(), projection); + + String[] expectedIgnored = { + "installed, already latest, ignore all", + "installed, already latest, ignore latest", + // NOT "installed, already latest, ignore old" - because it + // is should only ignore if "ignored version" is >= suggested + + "installed, old version, ignore all", + "installed, old version, ignore latest" + // NOT "installed, old version, ignore newer, but not latest" + // for the same reason as above. + }; + + assertContainsOnlyIds(ignoredApps, expectedIgnored); + } + + private void assertContainsOnlyIds(List actualApps, String[] expectedIds) { + List actualIds = new ArrayList(actualApps.size()); + for (App app : actualApps) { + actualIds.add(app.id); + } + TestUtils.assertContainsOnly(actualIds, expectedIds); + } + public void testInstalled() { Utils.clearInstalledApksCache(); diff --git a/test/src/org/fdroid/fdroid/TestUtils.java b/test/src/org/fdroid/fdroid/TestUtils.java index 6d9bbaa2a..1afc0454c 100644 --- a/test/src/org/fdroid/fdroid/TestUtils.java +++ b/test/src/org/fdroid/fdroid/TestUtils.java @@ -24,7 +24,7 @@ public class TestUtils { if (i > 0) { string += ", "; } - string += list.get(i); + string += "'" + list.get(i) + "'"; } string += "]"; return string; From 31fe8343adbd503ed7a8610f03ca00b9ea1745d6 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 14 Feb 2014 23:17:48 -0500 Subject: [PATCH 171/282] fix RO translation's formats, based on lint warning Inconsistent formatting types for argument #1 in format string searchres_napps ('%s'): Found both 'd' and 's' (in values/strings.xml) This lint check ensures the following: (1) If there are multiple translations of the format string, then all translations use the same type for the same numbered arguments (2) The usage of the format string in Java consistent with the format string, meaning that the parameter types passed to String.format matches those in the format string. Sa gasit o aplicatie potrivita cu %s\' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --- res/values-ro/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml index 40c1a4fe3..f0f0db7ae 100644 --- a/res/values-ro/strings.xml +++ b/res/values-ro/strings.xml @@ -1,8 +1,8 @@ - Sa gasit o aplicatie potrivita cu %s\' - Sa gasit o aplicatie potrivita cu %s\' - Nu exita aplicatii potrivite cu %s\': + Sa gasit %1$d aplicații potrivita cu \'%2$s\' + Sa gasit o aplicatie potrivita cu \'%s\' + Nu exita aplicatii potrivite cu \'%s\': Versiune Istoric aplicatii descarcate Noutati From 1d3c18423b51b7a1b1b8b7b747e11733e477b8f5 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 30 Jan 2014 21:25:50 -0500 Subject: [PATCH 172/282] use https for fdroid.org everywhere, avoid redirects --- res/values-ca/strings.xml | 2 +- res/values-el/strings.xml | 2 +- res/values-fr/strings.xml | 2 +- res/values-pt-rBR/strings.xml | 2 +- res/values-sr/strings.xml | 2 +- res/values-ug/strings.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index eed37b7a4..cccc8a333 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -29,7 +29,7 @@ Publicat amb la llicència GNU GPL v3. Un dipòsit és una font d\'aplicacions. Per afegir-ne un, premeu ara el botó MENÚ i entreu la seva URL. -L\'adreça d\'un dipòsit té un aspecte com ara: http://f-droid.org/repo +L\'adreça d\'un dipòsit té un aspecte com ara: https://f-droid.org/repo Instal·lat No està instal·lat S\'ha afegit a %s diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index 6a2b5e8f7..80f806187 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -29,7 +29,7 @@ Ένα αποθετήριο είναι μια πηγή εφαρμογών. Για να προσθέσετε κάποιο, πιέστε το πλήκτρο ΜΕΝΟΥ και εισάγετε το URL. -Μια διεύθυνση αποθετηρίου μοιάζει κάπως έτσι: http://f-droid.org/repo +Μια διεύθυνση αποθετηρίου μοιάζει κάπως έτσι: https://f-droid.org/repo Εγκατεστημένο Δεν είναι εγκατεστημένο Προστέθηκε στις %s diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 51677523d..2796c6b36 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -29,7 +29,7 @@ Publiée sous licence GNU GPL v3. Un dépôt est une source d\'applications. Pour en ajouter un, appuyez maintenant sur le bouton MENU et entrez l\'adresse URL. -L\'URL d\'un dépôt ressemble à ceci : http://f-droid.org/repo +L\'URL d\'un dépôt ressemble à ceci : https://f-droid.org/repo Installée Pas installée Ajouté le %s diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index 6c3b972bc..72164cc15 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -29,7 +29,7 @@ Lançado sob a licença GNU GPLv3. Um repositório é uma fonte de aplicativos. Para adicionar um, pressione o botão MENU e digite a URL. -Um endereço do repositório é algo similar a isto: http://f-droid.org/repo +Um endereço do repositório é algo similar a isto: https://f-droid.org/repo Instalado Não Instalado Adicionado em %s diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index fad6b6304..faf2f69a6 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -29,7 +29,7 @@ Ризнице су места одакле се скидају апликације. Да би сте додали једну, притисните тастер МЕНИ и унесите адресу. -Адреса ризнице би личила на ово: http://f-droid.org/repo +Адреса ризнице би личила на ово: https://f-droid.org/repo Инсталирана Није Инсталирана Додато %s diff --git a/res/values-ug/strings.xml b/res/values-ug/strings.xml index 53c8c774b..0b1921925 100644 --- a/res/values-ug/strings.xml +++ b/res/values-ug/strings.xml @@ -29,7 +29,7 @@ خەزىنە ئەپلەرنىڭ تارقىتىلىش مەنبەسى بولۇپ، مەنبە قوشۇشتا، تىزىملىك توپچىنى بېسىپ، ئاندىن URLنى كىرگۈزۈڭ. -خەزىنە ئادرېسى بۇنىڭغا ئوخشاش بولىدۇ: http://f-droid.org/repo +خەزىنە ئادرېسى بۇنىڭغا ئوخشاش بولىدۇ: https://f-droid.org/repo ئورنىتىلغان ئورنىتىلمىغان %s دا قوشۇلغان From 0f44c5edbac5f5ce7256f4d36bbb929f29bc65fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 10 Mar 2014 17:56:38 +0100 Subject: [PATCH 173/282] Fix scaling of icons on AppDetails --- res/layout/appdetails.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/layout/appdetails.xml b/res/layout/appdetails.xml index 9d48c5c0d..bd8721239 100644 --- a/res/layout/appdetails.xml +++ b/res/layout/appdetails.xml @@ -18,7 +18,7 @@ android:layout_height="56dp" android:layout_centerVertical="true" android:padding="4dp" - android:scaleType="center" /> + android:scaleType="fitCenter" /> Date: Mon, 10 Mar 2014 17:56:49 +0100 Subject: [PATCH 174/282] No need to reset views in the app lists --- src/org/fdroid/fdroid/views/AppListAdapter.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/org/fdroid/fdroid/views/AppListAdapter.java b/src/org/fdroid/fdroid/views/AppListAdapter.java index ca0336d87..fb9003586 100644 --- a/src/org/fdroid/fdroid/views/AppListAdapter.java +++ b/src/org/fdroid/fdroid/views/AppListAdapter.java @@ -48,7 +48,6 @@ abstract public class AppListAdapter extends CursorAdapter { .cacheInMemory(true) .cacheOnDisc(true) .imageScaleType(ImageScaleType.NONE) - .resetViewBeforeLoading(true) .showImageOnLoading(R.drawable.ic_repo_app_default) .showImageForEmptyUri(R.drawable.ic_repo_app_default) .displayer(new FadeInBitmapDisplayer(200, true, true, false)) From 6fa72607b8fb162909a16a7f5069d3b39855d5f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 10 Mar 2014 18:19:15 +0100 Subject: [PATCH 175/282] Fix relative layouts on <4.2, broken while trying to add RTL support --- res/layout/about.xml | 5 +++++ res/layout/addrepo.xml | 1 + res/layout/apklistitem.xml | 6 ++++++ res/layout/appdetails.xml | 9 +++++++++ res/layout/applistitem.xml | 11 +++++++++++ res/layout/repo_item.xml | 3 +++ res/layout/repodetails.xml | 10 +++++++++- res/layout/repodiscoveryitem.xml | 2 ++ res/layout/repodiscoverylist.xml | 2 ++ res/layout/searchresults.xml | 2 ++ 10 files changed, 50 insertions(+), 1 deletion(-) diff --git a/res/layout/about.xml b/res/layout/about.xml index 7a3837d06..203e19d3e 100644 --- a/res/layout/about.xml +++ b/res/layout/about.xml @@ -2,7 +2,9 @@ @@ -38,6 +41,7 @@ @@ -57,6 +61,7 @@ @@ -54,6 +58,7 @@ @@ -61,6 +66,7 @@ diff --git a/res/layout/appdetails.xml b/res/layout/appdetails.xml index bd8721239..352c8a687 100644 --- a/res/layout/appdetails.xml +++ b/res/layout/appdetails.xml @@ -24,6 +24,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" + android:layout_toRightOf="@id/icon" android:layout_toEndOf="@id/icon" android:padding="5dp" android:baselineAligned="false" @@ -34,9 +35,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" + android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:singleLine="true" android:ellipsize="end" + android:layout_marginLeft="6sp" android:layout_marginStart="6sp" android:textSize="12sp" /> @@ -48,8 +51,10 @@ android:ellipsize="end" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:textAlignment="viewStart" + android:layout_toLeftOf="@id/license" android:layout_toStartOf="@id/license" /> @@ -70,8 +77,10 @@ android:singleLine="true" android:ellipsize="end" android:textSize="12sp" + android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:textAlignment="viewStart" + android:layout_toLeftOf="@id/categories" android:layout_toStartOf="@id/categories" android:layout_below="@id/title" /> diff --git a/res/layout/applistitem.xml b/res/layout/applistitem.xml index 5ccf7f6dc..b697f3242 100644 --- a/res/layout/applistitem.xml +++ b/res/layout/applistitem.xml @@ -19,8 +19,11 @@ android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content" + android:paddingLeft="5dp" android:paddingStart="5dp" + android:paddingRight="5dp" android:paddingEnd="5dp" + android:layout_toRightOf="@id/icon" android:layout_toEndOf="@id/icon" android:layout_centerVertical="true" android:baselineAligned="false" > @@ -31,8 +34,10 @@ android:ellipsize="end" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginLeft="6sp" android:layout_marginStart="6sp" android:layout_alignParentTop="true" + android:layout_alignParentRight="true" android:layout_alignParentEnd="true" /> diff --git a/res/layout/repo_item.xml b/res/layout/repo_item.xml index 9c0e10951..c2e4bffa6 100644 --- a/res/layout/repo_item.xml +++ b/res/layout/repo_item.xml @@ -15,6 +15,7 @@ @@ -16,6 +18,7 @@ android:layout_height="wrap_content" android:id="@+id/label_repo_url" android:text="@string/repo_url" + android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_alignParentTop="true" /> - \ No newline at end of file + diff --git a/res/layout/repodiscoveryitem.xml b/res/layout/repodiscoveryitem.xml index b6039dfbd..4b0a215f0 100644 --- a/res/layout/repodiscoveryitem.xml +++ b/res/layout/repodiscoveryitem.xml @@ -8,6 +8,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLines="1" + android:paddingLeft="8sp" android:paddingStart="8sp" android:text="Discovered Repo Name" android:textSize="16sp" @@ -19,6 +20,7 @@ android:layout_height="wrap_content" android:layout_below="@+id/reposcanitemname" android:layout_marginTop="2dp" + android:paddingLeft="8sp" android:paddingStart="8sp" android:maxLines="1" android:text="Repo Address" diff --git a/res/layout/repodiscoverylist.xml b/res/layout/repodiscoverylist.xml index 0a8f46052..cdd85147d 100644 --- a/res/layout/repodiscoverylist.xml +++ b/res/layout/repodiscoverylist.xml @@ -8,6 +8,7 @@ android:layout_width="match_parent" android:layout_height="50dp" android:layout_alignParentTop="true" + android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_centerHorizontal="true" android:paddingTop="8sp" > @@ -18,6 +19,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" + android:layout_marginRight="5dp" android:layout_marginEnd="5dp" android:indeterminate="true" /> diff --git a/res/layout/searchresults.xml b/res/layout/searchresults.xml index 29ce735c6..41ba64b45 100644 --- a/res/layout/searchresults.xml +++ b/res/layout/searchresults.xml @@ -2,7 +2,9 @@ From be4db93da5b9b0294a189ff53ff21b359e73a872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 10 Mar 2014 18:27:43 +0100 Subject: [PATCH 176/282] Forgot the rtl fixes for layout-land --- res/layout-land/appdetails.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/layout-land/appdetails.xml b/res/layout-land/appdetails.xml index 03ab9b911..1aba209ed 100644 --- a/res/layout-land/appdetails.xml +++ b/res/layout-land/appdetails.xml @@ -15,6 +15,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="5dp" + android:layout_marginRight="4dp" android:layout_marginEnd="4dp" android:orientation="vertical" > @@ -44,6 +45,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:padding="4dp" + android:layout_toRightOf="@id/icon" android:layout_toEndOf="@id/icon" android:orientation="vertical" > From 54d78491915e2fce2dd19fcd158e6fd51baecf7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 10 Mar 2014 18:30:42 +0100 Subject: [PATCH 177/282] Get rid of lint TargetApi warnings --- .../fdroid/fdroid/NfcNotEnabledActivity.java | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/org/fdroid/fdroid/NfcNotEnabledActivity.java b/src/org/fdroid/fdroid/NfcNotEnabledActivity.java index 711a17f97..793949cc7 100644 --- a/src/org/fdroid/fdroid/NfcNotEnabledActivity.java +++ b/src/org/fdroid/fdroid/NfcNotEnabledActivity.java @@ -9,29 +9,38 @@ import android.os.Build; import android.os.Bundle; import android.provider.Settings; -@TargetApi(14) // aka Android 4.0 aka Ice Cream Sandwich public class NfcNotEnabledActivity extends Activity { + /* + * ACTION_NFC_SETTINGS was added in 4.1 aka Jelly Bean MR1 as a + * separate thing from ACTION_NFCSHARING_SETTINGS. It is now + * possible to have NFC enabled, but not "Android Beam", which is + * needed for NDEF. Therefore, we detect the current state of NFC, + * and steer the user accordingly. + */ + @TargetApi(14) + private void doOnJellybean(Intent intent) { + if (NfcAdapter.getDefaultAdapter(this).isEnabled()) + intent.setAction(Settings.ACTION_NFCSHARING_SETTINGS); + else + intent.setAction(Settings.ACTION_NFC_SETTINGS); + } + + // this API was added in 4.0 aka Ice Cream Sandwich + @TargetApi(16) + private void doOnIceCreamSandwich(Intent intent) { + intent.setAction(Settings.ACTION_NFCSHARING_SETTINGS); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final Intent intent = new Intent(); if (Build.VERSION.SDK_INT >= 16) { - /* - * ACTION_NFC_SETTINGS was added in 4.1 aka Jelly Bean MR1 as a - * separate thing from ACTION_NFCSHARING_SETTINGS. It is now - * possible to have NFC enabled, but not "Android Beam", which is - * needed for NDEF. Therefore, we detect the current state of NFC, - * and steer the user accordingly. - */ - if (NfcAdapter.getDefaultAdapter(this).isEnabled()) - intent.setAction(Settings.ACTION_NFCSHARING_SETTINGS); - else - intent.setAction(Settings.ACTION_NFC_SETTINGS); + doOnJellybean(intent); } else if (Build.VERSION.SDK_INT >= 14) { - // this API was added in 4.0 aka Ice Cream Sandwich - intent.setAction(Settings.ACTION_NFCSHARING_SETTINGS); + doOnIceCreamSandwich(intent); } else { // no NFC support, so nothing to do here finish(); From 49a3c3370f35b6104e53ab27a3eb1df4ebc94f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 10 Mar 2014 18:36:35 +0100 Subject: [PATCH 178/282] Don't finish the whole Repositores activity when cancelling "New Repository" --- src/org/fdroid/fdroid/views/fragments/RepoListFragment.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java index 949474441..c30f23397 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java @@ -408,9 +408,7 @@ public class RepoListFragment extends ListFragment new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - getActivity().setResult(Activity.RESULT_CANCELED); - getActivity().finish(); - return; + dialog.dismiss(); } }); alrt.show(); From 750da53970dedb3f980af7e4a01b22bde3ead487 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Sun, 16 Mar 2014 13:40:20 +0100 Subject: [PATCH 179/282] Add Android.mk for building in ROMs Android.mk is needed to build F-Droid as part of other ROMs. A ROM would have to emulate the .gitmodules with repo. Note: the build will fail until AndroidPinning pulls a trivial fix for super(null). There is also a layout bug that is fixed by the next commit. --- Android.mk | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Android.mk diff --git a/Android.mk b/Android.mk new file mode 100644 index 000000000..b15a70425 --- /dev/null +++ b/Android.mk @@ -0,0 +1,23 @@ +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_PACKAGE_NAME := F-Droid +LOCAL_CERTIFICATE := platform +LOCAL_MODULE_TAGS := optional +LOCAL_SRC_FILES := \ + $(call all-java-files-under, src) \ + $(call all-java-files-under, extern/MemorizingTrustManager/src) \ + $(call all-java-files-under, extern/AndroidPinning/src) \ + $(call all-java-files-under, extern/UniversalImageLoader/library/src ) + +res_dirs = res extern/MemorizingTrustManager/res extern/AndroidPinning/res +LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) + +LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4 + +LOCAL_AAPT_FLAGS := --auto-add-overlay +LOCAL_AAPT_FLAGS += --extra-packages org.fdroid.fdroid:de.duenndns.ssl:org.thoughtcrime.ssl.pinning + +LOCAL_PRIVILEGED_MODULE := true +include $(BUILD_PACKAGE) + From d55fc7cd6954cdfeaae41d8ed294253ec80b465e Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Sun, 16 Mar 2014 13:44:25 +0100 Subject: [PATCH 180/282] Fix build failure with newer toolchains. Android somehow wants every string to be externalized on newer toolchains, let's prepare for that. This fixes in-tree building of f-droid. --- res/layout/repodiscoveryitem.xml | 4 ++-- res/values/strings.xml | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/res/layout/repodiscoveryitem.xml b/res/layout/repodiscoveryitem.xml index 4b0a215f0..c7fc54f88 100644 --- a/res/layout/repodiscoveryitem.xml +++ b/res/layout/repodiscoveryitem.xml @@ -10,7 +10,7 @@ android:maxLines="1" android:paddingLeft="8sp" android:paddingStart="8sp" - android:text="Discovered Repo Name" + android:text="@string/discovered_repo_name" android:textSize="16sp" android:textStyle="bold" /> @@ -23,7 +23,7 @@ android:paddingLeft="8sp" android:paddingStart="8sp" android:maxLines="1" - android:text="Repo Address" + android:text="@string/repo_address" android:textSize="14sp" /> diff --git a/res/values/strings.xml b/res/values/strings.xml index 54b027945..3ff4a69d4 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -210,4 +210,7 @@ Your device is not on the same WiFi as the local repo you just added! Try joining this network: %s Requires: %1$s + Discovered Repo Name + Repo Address + From 61bbbf442dce7abb48b1704713514ec59e47ce20 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Sun, 16 Mar 2014 13:50:17 +0100 Subject: [PATCH 181/282] Make the list of default repos dynamic This patch iterates over the configured list of repos and adds them to the db on create. This means that the initial list of repositories is now fully configurable. Added the guardian project repo (disabled) as a testcase. --- res/values/default_repo.xml | 17 +++-- src/org/fdroid/fdroid/data/DBHelper.java | 80 +++++++++++++----------- 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/res/values/default_repo.xml b/res/values/default_repo.xml index a94f210b3..ea4b96cee 100644 --- a/res/values/default_repo.xml +++ b/res/values/default_repo.xml @@ -1,10 +1,19 @@ - F-Droid + 3 + F-Droid F-Droid Archive - https://f-droid.org/repo + GuardianProject + 1 + 0 + 0 + https://f-droid.org/repo https://f-droid.org/archive - The official FDroid repository. Applications in this repository are mostly built directory from the source code. Some are official binaries built by the original application developers - these will be replaced by source-built versions over time. + https://guardianproject.info/repo/ + The official FDroid repository. Applications in this repository are mostly built directory from the source code. Some are official binaries built by the original application developers - these will be replaced by source-built versions over time. The archive repository of the F-Droid client. This contains older versions of applications from the main repository. - 3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef + A curated repository of apps developed by the Guardian Project and others focused on mobile security, privacy and safety. + 3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef + 3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef + 308203c5308202ada00302010202047b7cf549300d06092a864886f70d01010b0500308192310b30090603550406130255533111300f060355040813084e657720596f726b3111300f060355040713084e657720596f726b311d301b060355040a131454686520477561726469616e2050726f6a656374311f301d060355040b1316477561726469616e20462d44726f6964204275696c64311d301b06035504031314677561726469616e70726f6a6563742e696e666f301e170d3132313032393130323530305a170d3430303331363130323530305a308192310b30090603550406130255533111300f060355040813084e657720596f726b3111300f060355040713084e657720596f726b311d301b060355040a131454686520477561726469616e2050726f6a656374311f301d060355040b1316477561726469616e20462d44726f6964204275696c64311d301b06035504031314677561726469616e70726f6a6563742e696e666f30820122300d06092a864886f70d01010105000382010f003082010a0282010100b7f1f635fa3fce1a8042aaa960c2dc557e4ad2c082e5787488cba587fd26207cf59507919fc4dcebda5c8c0959d14146d0445593aa6c29dc639570b71712451fd5c231b0c9f5f0bec380503a1c2a3bc00048bc5db682915afa54d1ecf67b45e1e05c0934b3037a33d3a565899131f27a72c03a5de93df17a2376cc3107f03ee9d124c474dfab30d4053e8f39f292e2dcb6cc131bce12a0c5fc307985195d256bf1d7a2703d67c14bf18ed6b772bb847370b20335810e337c064fef7e2795a524c664a853cd46accb8494f865164dabfb698fa8318236432758bc40d52db00d5ce07fe2210dc06cd95298b4f09e6c9b7b7af61c1d62ea43ea36a2331e7b2d4e250203010001a321301f301d0603551d0e0416041404d763e981cf3a295b94a790d8536a783097232b300d06092a864886f70d01010b05000382010100654e6484ff032c54fed1d96d3c8e731302be9dbd7bb4fe635f2dac05b69f3ecbb5acb7c9fe405e2a066567a8f5c2beb8b199b5a4d5bb1b435cf02df026d4fb4edd9d8849078f085b00950083052d57467d65c6eebd98f037cff9b148d621cf8819c4f7dc1459bf8fc5c7d76f901495a7caf35d1e5c106e1d50610c4920c3c1b50adcfbd4ad83ce7353cdea7d856bba0419c224f89a2f3ebc203d20eb6247711ad2b55fd4737936dc42ced7a047cbbd24012079204a2883b6d55d5d5b66d9fd82fb51fca9a5db5fad9af8564cb380ff30ae8263dbbf01b46e01313f53279673daa3f893380285646b244359203e7eecde94ae141b7dfa8e6499bb8e7e0b25ab85 diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index 43a435efd..8f00eeac6 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -2,6 +2,7 @@ package org.fdroid.fdroid.data; import android.content.ContentValues; import android.content.Context; +import android.content.res.Resources; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; @@ -175,40 +176,49 @@ public class DBHelper extends SQLiteOpenHelper { createAppApk(db); db.execSQL(CREATE_TABLE_REPO); - ContentValues values = new ContentValues(); - values.put("address", - context.getString(R.string.default_repo_address)); - values.put("name", - context.getString(R.string.default_repo_name)); - values.put("description", - context.getString(R.string.default_repo_description)); - values.put("version", 0); - String pubkey = context.getString(R.string.default_repo_pubkey); - String fingerprint = Utils.calcFingerprint(pubkey); - values.put("pubkey", pubkey); - values.put("fingerprint", fingerprint); - values.put("maxage", 0); - values.put("inuse", 1); - values.put("priority", 10); - values.put("lastetag", (String) null); - db.insert(TABLE_REPO, null, values); - values = new ContentValues(); - values.put("address", - context.getString(R.string.default_repo_address2)); - values.put("name", - context.getString(R.string.default_repo_name2)); - values.put("description", - context.getString(R.string.default_repo_description2)); - values.put("version", 0); - // default #2 is /archive which has the same key as /repo - values.put("pubkey", pubkey); - values.put("fingerprint", fingerprint); - values.put("maxage", 0); - values.put("inuse", 0); - values.put("priority", 20); - values.put("lastetag", (String) null); - db.insert(TABLE_REPO, null, values); + Resources ress = context.getResources(); + + int repoCount = ress.getInteger(R.integer.default_repo_count); + for (int i = 1; i <= repoCount; i++) { + ContentValues values = new ContentValues(); + String repoName = context.getString(ress.getIdentifier( + "default_repo_name" + i, + "string", + "org.fdroid.fdroid" + )); + values.put("address", + context.getString(ress.getIdentifier( + "default_repo_address" + i, + "string", + "org.fdroid.fdroid" + ))); + values.put("name",repoName); + values.put("description", + context.getString(ress.getIdentifier( + "default_repo_description" + i, + "string", + "org.fdroid.fdroid" + ))); + String pubkey = context.getString(ress.getIdentifier( + "default_repo_pubkey" + i, + "string", + "org.fdroid.fdroid" + )); + String fingerprint = Utils.calcFingerprint(pubkey); + values.put("pubkey", pubkey); + values.put("fingerprint", fingerprint); + values.put("maxage", 0); + values.put("inuse", ress.getInteger(ress.getIdentifier( + "default_repo_inuse" + i, + "integer", + "org.fdroid.fdroid" + ))); + values.put("priority", 10); + values.put("lastetag", (String) null); + Log.i("FDroid", "Add repository " + repoName); + db.insert(TABLE_REPO, null, values); + } } @Override @@ -280,8 +290,8 @@ public class DBHelper extends SQLiteOpenHelper { if (!descriptionExists) db.execSQL("alter table " + TABLE_REPO + " add column description text"); ContentValues values = new ContentValues(); - values.put("name", context.getString(R.string.default_repo_name)); - values.put("description", context.getString(R.string.default_repo_description)); + values.put("name", context.getString(R.string.default_repo_name1)); + values.put("description", context.getString(R.string.default_repo_description1)); db.update(TABLE_REPO, values, "address = ?", new String[]{ context.getString(R.string.default_repo_address)}); values.clear(); From 1f799d1ef1a9f37b72cb05d2e1b9e41c9a451d1b Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Sun, 16 Mar 2014 13:52:05 +0100 Subject: [PATCH 182/282] Say that f-droid is not an unknown source. This property will be ignored if f-droid is not installed as priv-app, but it _will_ skip the "you have to enable unknown sources" dialog if f-droid is installed as priv-app. There is thus no gain in keeping it as is (false). --- src/org/fdroid/fdroid/AppDetails.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 2e8f49867..9f5eb2d14 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -913,6 +913,7 @@ public class AppDetails extends ListActivity { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(Uri.parse("file://" + file.getPath()), "application/vnd.android.package-archive"); + intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); startActivityForResult(intent, REQUEST_INSTALL); ((FDroidApp) getApplication()).invalidateApp(id); } From 7fa25d3209cb9eb109ceef5c370bed1fee8c89e3 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Sun, 16 Mar 2014 14:03:46 +0100 Subject: [PATCH 183/282] Add ROM building instructions Android.mk enables ROM devs to bundle F-Droid. Add instruction on how to do it. --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 9c5d1fbbb..56db155f6 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,26 @@ The project itself supports Gradle, but some of the libraries it uses don't. Hence it is currently not possible to build F-Droid with Gradle in a clean way without manual interaction. +Building as part of a ROM +------------------------- + +Add the following lines to your repo manifest + +``` + + + + + + + + + + + +``` + +Adding F-Droid is then just a matter of adding "F-Droid" to your PRODUCT_PACKAGES. Direct download --------------- From 67e10206847d1ba3276bbe36c02b9cc65da10b70 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Sun, 16 Mar 2014 14:20:44 +0100 Subject: [PATCH 184/282] Add missed occurance of old repo address R.string --- src/org/fdroid/fdroid/data/DBHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index 8f00eeac6..6807de9cc 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -293,7 +293,7 @@ public class DBHelper extends SQLiteOpenHelper { values.put("name", context.getString(R.string.default_repo_name1)); values.put("description", context.getString(R.string.default_repo_description1)); db.update(TABLE_REPO, values, "address = ?", new String[]{ - context.getString(R.string.default_repo_address)}); + context.getString(R.string.default_repo_address1)}); values.clear(); values.put("name", context.getString(R.string.default_repo_name2)); values.put("description", context.getString(R.string.default_repo_description2)); From 0ec4e3756d2442367ee1fe178519c0e6e063f6d2 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 7 Mar 2014 08:18:28 +1100 Subject: [PATCH 185/282] Don't show update option in list if ignored. Related to the last bug with the update notify count. This one is also due to the fact we didn't ask for the right data from the provider. If these bugs keep coming in over time, I will seriously consider guarding access to each variable with a check, and throwing an exception if the variable hasn't been initialized. For now I'll see if it was a once off. Hopefullly tests will catch these issues in the future. --- src/org/fdroid/fdroid/views/fragments/AppListFragment.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java index b3f2a91a4..c6c674575 100644 --- a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java @@ -37,6 +37,8 @@ abstract public class AppListFragment extends ListFragment implements AppProvider.DataColumns.ICON_URL, AppProvider.DataColumns.SuggestedApk.VERSION, AppProvider.DataColumns.SUGGESTED_VERSION_CODE, + AppProvider.DataColumns.IGNORE_ALLUPDATES, + AppProvider.DataColumns.IGNORE_THISUPDATE, AppProvider.DataColumns.REQUIREMENTS, // Needed for filtering apps that require root. }; From 8acb1b9a767a437ee2f857d41429b816998e711a Mon Sep 17 00:00:00 2001 From: F-Droid Translatebot Date: Mon, 17 Mar 2014 08:24:02 +0000 Subject: [PATCH 186/282] Translation updates --- res/values-ca/strings.xml | 13 ++++++++++++- res/values-de/strings.xml | 16 ++++++++++++++++ res/values-fr/strings.xml | 37 +++++++++++++++++++++++++++++++++++++ res/values-ru/strings.xml | 3 ++- 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index cccc8a333..d909991ef 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -7,6 +7,8 @@ Sembla que aquest paquet no és compatible amb el vostre dispositiu. Voleu provar a instaŀlar-lo de totes maneres? Aneu a desactualitzar aquesta aplicació. Això podria fer que l\'aplicació no funcionés o inclús es perdessin les vostres dades. Esteu segur que ho voleu fer? Versió + Editar + Esborrar Memòria cau de les aplicacions baixades Actualitzacions Altres @@ -39,6 +41,8 @@ L\'adreça d\'un dipòsit té un aspecte com ara: https://f-droid.org/repoAfegeix un nou dipòsit Afegeix Anul·la + Permès + Afegeix clau Sobreescriu Trieu el dipòsit que voleu suprimir Actualitza els dipòsits @@ -51,7 +55,7 @@ L\'adreça d\'un dipòsit té un aspecte com ara: https://f-droid.org/repoS\'està actualitzant la llista d\'aplicacions… S\'està obtenint l\'aplicació des de Adreça del dipòsit - Aquest repositori ja existeix. + Aquest dipòsit ja existeix. La llista de dipòsits ha canviat. La voleu actualitzar? Actualitza els dipòsits @@ -61,6 +65,7 @@ La voleu actualitzar? Cerca Nou dipòsit Suprimeix el dipòsit + Cerca dipòsits locals Executa Comparteix Instal·la @@ -106,4 +111,10 @@ La voleu actualitzar? No teniu cap aplicació disponible que pugui gestionar %s Vista compacta Tema + URL + Número d\'aplicacions + Descripció + Actualitzant + Nom + Esborrar dipòsit? diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index fc92f4380..c9d95d28e 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -9,6 +9,7 @@ Version Bearbeiten Löschen + Aktiviere NFC Transfer... Anwendungszwischenspeicher Heruntergeladene Programmpakete auf der SD-Karte behalten Installationsdateien nicht behalten @@ -64,6 +65,10 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Bitte warten Anwendungsliste wird aktualisiert … Anwendung wird heruntergeladen von + NFC ist deaktiviert! + Öffne NFC Einstellungen... + Keine Bluetooth Verbindung gefunden. Bitte wählen Sie eine Verbindung aus! + Wähle Bluetooth Übertragung Adresse der Paketquelle Fingerabdruck (optional) Diese Paketquelle existiert bereits! @@ -71,15 +76,18 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Diese Paketquelle ist bereits eingerichtet, bestätigen, dass Sie diese wieder aktivieren möchten. Die eingehende Paketquelle ist bereits eingerichtet und aktiviert! Sie müssen diese Paketquelle zuerst löschen, bevor Sie eine mit einem anderen Schlüssel hinzuzufügen! + Ignoriere fehlerhafte Paketquellen URI: %s Die Liste der genutzten Paketquellen hat sich geändert. Sollen diese aktualisiert werden? Paketquellen aktualisieren Paketquellen verwalten + Bluetooth FDroid.apk… Einstellungen Über Suchen Paketquelle hinzufügen Paketquelle entfernen + Finde lokale Paketquellen Starten Empfehlen Installieren @@ -119,6 +127,8 @@ Sollen diese aktualisiert werden? Alle Was gibt es Neues Kürzlich Aktualisiert + Lokale F-Droid Paketquellen + Entdecke lokale F-Droid Paketquellen... Herunterladen %2$s / %3$s (%4$d%%) von %1$s @@ -128,6 +138,7 @@ Sollen diese aktualisiert werden? Verbinden mit %1$s Kompatibilität mit Ihrem Gerät wird überprüft … + Speichere App-Details (%1$d%%) Es werden keine Berechtigungen verwendet. Berechtigungen für Version %s Berechtigungen anzeigen @@ -141,6 +152,7 @@ Sollen diese aktualisiert werden? Nicht signiert Adresse Anwendungsanzahl + Fingerabdruck des Signaturschlüssels (SHA-256) Beschreibung Letzte Aktualisierung Aktualisierung @@ -170,4 +182,8 @@ Bemerkung: Alle Sie müssen diese Paketquelle wieder aktivieren, um Anwendungen daraus installieren zu können. %s oder später + bis %s + %1$s bis %2$s + Dieses Gerät befindet sich nicht im selben WLAN wie die eben hinzugefügte Paketquelle. Versuchen Sie sich mit dem folgenden Netzwek zu verbinden: %s + Erfordert: %1$s diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 2796c6b36..12651403d 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -7,13 +7,20 @@ Il semble que ce paquet ne soit pas compatible avec votre appareil. Voulez-vous quand même tenter de l\'installer ? Vous essayez de revenir à une ancienne version de cette application. Cela peut causer des problèmes de fonctionnement ou des pertes de données. Voulez-vous tout de même revenir à une ancienne version? Version + Éditer + Supprimer + Activer l\'envoi NFC… Stocker les applications téléchargées sur l\'appareil + Garder les fichiers apk téléchargés sur la carte SD + Ne pas garder les fichiers apk Mises à jour Autres Dernière analyse du dépôt : %s jamais Seulement par WiFi + Toujours mettre à jour automatiquement les listes d\'apps Notifier + Ne pas notifier des mises à jour Historique des mises à jour Résultats de la recherche Détails de l\'application @@ -39,6 +46,9 @@ L\'URL d\'un dépôt ressemble à ceci : https://f-droid.org/repo Ajouter un nouveau dépôt Ajouter Annuler + Activer + Ajouter une clé + Écraser Choisissez le dépôt à supprimer Mettre à jour les dépôts Disponible @@ -49,7 +59,15 @@ L\'URL d\'un dépôt ressemble à ceci : https://f-droid.org/repo Patientez Mise à jour de la liste d\'applications… Réception d\'application de + Le NFC n\'est pas activé ! + Allez dans les paramètres NFC… + Pas de méthode d\'envoi Bluetooth trouvée, choisissez en une ! + Choisir la méthode d\'envoi Bluetooth Adresse du dépôt + Empreinte digitale (optionnel) + Ce dépôt existe déja ! + Ce dépôt est déja configuré, confirmer que vous voulez le réactiver. + Vous devez d\'abord supprimer ce dépôt avant d\'en ajouter avec une clé différente. La liste des dépôts utilisés a changé. Voulez-vous les mettre à jour ? Mettre à jour les dépôts @@ -59,6 +77,7 @@ Voulez-vous les mettre à jour ? Rechercher Nouveau dépôt Supprimer un dépôt + Trouver des dépôts locaux Lancer Partager Installer @@ -81,14 +100,17 @@ Voulez-vous les mettre à jour ? Cette application dépend d\'autres applications non libres Affichage Expert + Cacher des extras pour les utilisateurs avancés Rechercher des applications Compatibilité de l\'application Versions incompatibles Root Ignorer l\'écran tactile + Filtrer les apps normalement Tout Quoi de neuf ? Mis à jour récemment + Dépôt FDroid locaux Téléchargement %2$s / %3$s (%4$d%%) de %1$s @@ -98,10 +120,25 @@ Voulez-vous les mettre à jour ? Connexion à %1$s Vérification de la compatibilité des applis avec votre appareil… + Sauvegarder les détails de l\'application (%1$d%%) Aucune autorisation n\'est utilisée. Autorisations pour la version %s Afficher les autorisations Vous n\'avez aucune application installée pour gérer %s Affichage compact + Montrer les icônes à une taille plus petite + Montrer les icônes à une taille normale Thème + Non signé + URL + Nombre d\'applications + Description + Dernière mise à jour + Nom + Voulez vous supprimer le dépôt \"{0}\", qui a {1} apps ? Toutes les applications installées ne seront pas supprimés, mais vous ne serez plus en mesure de les mettre à jour via F-Droid. + Inconnu + Supprimer le dépôt ? + Supprimer un dépôt signifie que les apps ne seront disponibles via F-Droid. Note: Toute les apps précédemment installées vont rester sur votre appareil. + jusqu\'à %s + Nécessite: %1$s diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 6bd3efce8..057c9d14b 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -126,7 +126,7 @@ %1$s Соединение с %1$s - Проверка совместимости приложений с устройством… + Проверка совместимости приложений с вашим устройством… Разрешений не требуется. Разрешения для версии %s Показывать разрешения @@ -154,4 +154,5 @@ Примечание: Все ранее установленные приложения будут оставаться на вашем устройстве. \"%1$s\" отключен. Вам нужно повторно включить этот репозиторий для установки приложений из него. %s или позднее + до %s From 2095229061fc38e954f177346036a7917ee80980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 17 Mar 2014 09:26:23 +0100 Subject: [PATCH 187/282] Run translation auto-correct scripts --- res/values-de/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index c9d95d28e..b8d72f794 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -9,7 +9,7 @@ Version Bearbeiten Löschen - Aktiviere NFC Transfer... + Aktiviere NFC Transfer… Anwendungszwischenspeicher Heruntergeladene Programmpakete auf der SD-Karte behalten Installationsdateien nicht behalten @@ -66,7 +66,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Anwendungsliste wird aktualisiert … Anwendung wird heruntergeladen von NFC ist deaktiviert! - Öffne NFC Einstellungen... + Öffne NFC Einstellungen… Keine Bluetooth Verbindung gefunden. Bitte wählen Sie eine Verbindung aus! Wähle Bluetooth Übertragung Adresse der Paketquelle @@ -128,7 +128,7 @@ Sollen diese aktualisiert werden? Was gibt es Neues Kürzlich Aktualisiert Lokale F-Droid Paketquellen - Entdecke lokale F-Droid Paketquellen... + Entdecke lokale F-Droid Paketquellen… Herunterladen %2$s / %3$s (%4$d%%) von %1$s From a184ce7268ae9ed231ff64e2abc9953d27b07393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 17 Mar 2014 09:38:11 +0100 Subject: [PATCH 188/282] Make priorities configurable too --- res/values/default_repo.xml | 3 +++ src/org/fdroid/fdroid/data/DBHelper.java | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/res/values/default_repo.xml b/res/values/default_repo.xml index ea4b96cee..7cab859e3 100644 --- a/res/values/default_repo.xml +++ b/res/values/default_repo.xml @@ -7,6 +7,9 @@ 1 0 0 + 10 + 20 + 30 https://f-droid.org/repo https://f-droid.org/archive https://guardianproject.info/repo/ diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index 6807de9cc..9335fe654 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -214,7 +214,11 @@ public class DBHelper extends SQLiteOpenHelper { "integer", "org.fdroid.fdroid" ))); - values.put("priority", 10); + values.put("priority", ress.getInteger(ress.getIdentifier( + "default_repo_pubkey" + i, + "integer", + "org.fdroid.fdroid" + ))); values.put("lastetag", (String) null); Log.i("FDroid", "Add repository " + repoName); db.insert(TABLE_REPO, null, values); From 3df221bc4a2f6482a10bc25b384152d0674af9e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 17 Mar 2014 09:38:38 +0100 Subject: [PATCH 189/282] Don't ship third-party repos --- res/values/default_repo.xml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/res/values/default_repo.xml b/res/values/default_repo.xml index 7cab859e3..b6c463bea 100644 --- a/res/values/default_repo.xml +++ b/res/values/default_repo.xml @@ -1,22 +1,16 @@ - 3 + 2 F-Droid F-Droid Archive - GuardianProject 1 0 - 0 10 20 - 30 https://f-droid.org/repo https://f-droid.org/archive - https://guardianproject.info/repo/ The official FDroid repository. Applications in this repository are mostly built directory from the source code. Some are official binaries built by the original application developers - these will be replaced by source-built versions over time. The archive repository of the F-Droid client. This contains older versions of applications from the main repository. - A curated repository of apps developed by the Guardian Project and others focused on mobile security, privacy and safety. 3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef 3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef - 308203c5308202ada00302010202047b7cf549300d06092a864886f70d01010b0500308192310b30090603550406130255533111300f060355040813084e657720596f726b3111300f060355040713084e657720596f726b311d301b060355040a131454686520477561726469616e2050726f6a656374311f301d060355040b1316477561726469616e20462d44726f6964204275696c64311d301b06035504031314677561726469616e70726f6a6563742e696e666f301e170d3132313032393130323530305a170d3430303331363130323530305a308192310b30090603550406130255533111300f060355040813084e657720596f726b3111300f060355040713084e657720596f726b311d301b060355040a131454686520477561726469616e2050726f6a656374311f301d060355040b1316477561726469616e20462d44726f6964204275696c64311d301b06035504031314677561726469616e70726f6a6563742e696e666f30820122300d06092a864886f70d01010105000382010f003082010a0282010100b7f1f635fa3fce1a8042aaa960c2dc557e4ad2c082e5787488cba587fd26207cf59507919fc4dcebda5c8c0959d14146d0445593aa6c29dc639570b71712451fd5c231b0c9f5f0bec380503a1c2a3bc00048bc5db682915afa54d1ecf67b45e1e05c0934b3037a33d3a565899131f27a72c03a5de93df17a2376cc3107f03ee9d124c474dfab30d4053e8f39f292e2dcb6cc131bce12a0c5fc307985195d256bf1d7a2703d67c14bf18ed6b772bb847370b20335810e337c064fef7e2795a524c664a853cd46accb8494f865164dabfb698fa8318236432758bc40d52db00d5ce07fe2210dc06cd95298b4f09e6c9b7b7af61c1d62ea43ea36a2331e7b2d4e250203010001a321301f301d0603551d0e0416041404d763e981cf3a295b94a790d8536a783097232b300d06092a864886f70d01010b05000382010100654e6484ff032c54fed1d96d3c8e731302be9dbd7bb4fe635f2dac05b69f3ecbb5acb7c9fe405e2a066567a8f5c2beb8b199b5a4d5bb1b435cf02df026d4fb4edd9d8849078f085b00950083052d57467d65c6eebd98f037cff9b148d621cf8819c4f7dc1459bf8fc5c7d76f901495a7caf35d1e5c106e1d50610c4920c3c1b50adcfbd4ad83ce7353cdea7d856bba0419c224f89a2f3ebc203d20eb6247711ad2b55fd4737936dc42ced7a047cbbd24012079204a2883b6d55d5d5b66d9fd82fb51fca9a5db5fad9af8564cb380ff30ae8263dbbf01b46e01313f53279673daa3f893380285646b244359203e7eecde94ae141b7dfa8e6499bb8e7e0b25ab85 From 05edd59b0548f6ab043cb58a0d6b8b41022f18f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 17 Mar 2014 09:39:52 +0100 Subject: [PATCH 190/282] Ignore UnusedResources on default_repo.xml We iterate over them programmatically, so lint thinks we don't use them --- lint.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lint.xml b/lint.xml index 6401ff7f9..d8c98cba9 100644 --- a/lint.xml +++ b/lint.xml @@ -5,4 +5,7 @@ + + + From 58609a4f50b537572aa4d46bd228073c7393e681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 17 Mar 2014 13:20:38 +0100 Subject: [PATCH 191/282] Update submodules --- extern/UniversalImageLoader | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/UniversalImageLoader b/extern/UniversalImageLoader index aa1cde8c2..69aabb4f7 160000 --- a/extern/UniversalImageLoader +++ b/extern/UniversalImageLoader @@ -1 +1 @@ -Subproject commit aa1cde8c20f6ef57de994ce8f72f772a14800706 +Subproject commit 69aabb4f7b1834c09106c9983f99b648fab65791 From ca0ed2844ee84e4244b9236c760d343fa8a54cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 17 Mar 2014 13:35:35 +0100 Subject: [PATCH 192/282] README: Minor fixes, don't use repos differing from the ones in git submodules --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 56db155f6..73d68e3ec 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Building from source The only required tools are the Android SDK and Apache Ant. +Once you have checked out the version you wish to build, run: + ``` git submodule update --init ./ant-prepare.sh # This runs 'android update' on the libs and the main project @@ -23,7 +25,7 @@ without manual interaction. Building as part of a ROM ------------------------- -Add the following lines to your repo manifest +Add the following lines to your repo manifest: ``` @@ -35,11 +37,10 @@ Add the following lines to your repo manifest - - + ``` -Adding F-Droid is then just a matter of adding "F-Droid" to your PRODUCT_PACKAGES. +Adding F-Droid is then just a matter of adding `F-Droid` to your `PRODUCT_PACKAGES`. Direct download --------------- From 12a9a1cf2915ec81aa30193329bf3f31b5941e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 17 Mar 2014 13:41:03 +0100 Subject: [PATCH 193/282] New script: Update repo xml data with git repo/submodule data --- tools/repo-revisions.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100755 tools/repo-revisions.sh diff --git a/tools/repo-revisions.sh b/tools/repo-revisions.sh new file mode 100755 index 000000000..33b2d02dc --- /dev/null +++ b/tools/repo-revisions.sh @@ -0,0 +1,10 @@ +#!/bin/bash -ex + +# Update README repo manifest revisions from git + +LAST_STABLE_TAG=$(git describe --abbrev=0 --tags --match='*[0-9]') +sed -i 's@\(.*name="fdroidclient\.git".*revision="\)[^"]*\(".*\)@\1'$LAST_STABLE_TAG'\2@' README.md + +git ls-tree $LAST_STABLE_TAG extern/ | while read _ _ revision path; do + sed -i 's@\(.*fdroidclient/'$path'".*revision="\)[^"]*\(".*\)@\1'$revision'\2@' README.md +done From aa85cddd842873e43869b616a199272eb8918be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 17 Mar 2014 14:01:17 +0100 Subject: [PATCH 194/282] Fix repo manifest for 0.58, not master --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 73d68e3ec..ee5669007 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,9 @@ Add the following lines to your repo manifest: - + - - - - - + ``` Adding F-Droid is then just a matter of adding `F-Droid` to your `PRODUCT_PACKAGES`. From 7f652f8620b1f958c1a39bd1050ddec7b955bd09 Mon Sep 17 00:00:00 2001 From: F-Droid Translatebot Date: Mon, 17 Mar 2014 15:27:41 +0000 Subject: [PATCH 195/282] Translation updates --- res/values-de/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index b8d72f794..c9d95d28e 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -9,7 +9,7 @@ Version Bearbeiten Löschen - Aktiviere NFC Transfer… + Aktiviere NFC Transfer... Anwendungszwischenspeicher Heruntergeladene Programmpakete auf der SD-Karte behalten Installationsdateien nicht behalten @@ -66,7 +66,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Anwendungsliste wird aktualisiert … Anwendung wird heruntergeladen von NFC ist deaktiviert! - Öffne NFC Einstellungen… + Öffne NFC Einstellungen... Keine Bluetooth Verbindung gefunden. Bitte wählen Sie eine Verbindung aus! Wähle Bluetooth Übertragung Adresse der Paketquelle @@ -128,7 +128,7 @@ Sollen diese aktualisiert werden? Was gibt es Neues Kürzlich Aktualisiert Lokale F-Droid Paketquellen - Entdecke lokale F-Droid Paketquellen… + Entdecke lokale F-Droid Paketquellen... Herunterladen %2$s / %3$s (%4$d%%) von %1$s From 5e30d0d21897953146d4ebd2c4020359659a635c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 18 Mar 2014 08:15:04 +0100 Subject: [PATCH 196/282] Remove "Android App:" when sharing an application Reasons to do so: * Redundant * Often noisy * Not properly translated --- src/org/fdroid/fdroid/AppDetails.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 9f5eb2d14..3ecc8f4fe 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -927,7 +927,7 @@ public class AppDetails extends ListActivity { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); - shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Android App: "+app.name); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, app.name); shareIntent.putExtra(Intent.EXTRA_TEXT, app.name+" ("+app.summary+") - https://f-droid.org/app/"+app.id); startActivity(Intent.createChooser(shareIntent, getString(R.string.menu_share))); From 53a10aa44f713058266934744ae6ee3328478034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 18 Mar 2014 08:17:34 +0100 Subject: [PATCH 197/282] Re-run fix-ellipsis after translatebot overwrite --- res/values-de/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index c9d95d28e..b8d72f794 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -9,7 +9,7 @@ Version Bearbeiten Löschen - Aktiviere NFC Transfer... + Aktiviere NFC Transfer… Anwendungszwischenspeicher Heruntergeladene Programmpakete auf der SD-Karte behalten Installationsdateien nicht behalten @@ -66,7 +66,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Anwendungsliste wird aktualisiert … Anwendung wird heruntergeladen von NFC ist deaktiviert! - Öffne NFC Einstellungen... + Öffne NFC Einstellungen… Keine Bluetooth Verbindung gefunden. Bitte wählen Sie eine Verbindung aus! Wähle Bluetooth Übertragung Adresse der Paketquelle @@ -128,7 +128,7 @@ Sollen diese aktualisiert werden? Was gibt es Neues Kürzlich Aktualisiert Lokale F-Droid Paketquellen - Entdecke lokale F-Droid Paketquellen... + Entdecke lokale F-Droid Paketquellen… Herunterladen %2$s / %3$s (%4$d%%) von %1$s From fa8052611e8a2d1c17300ce7d90d2a420483bd1b Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 19 Mar 2014 06:22:07 +1100 Subject: [PATCH 198/282] Don't reset "transient" tables from now on. Instead, use: if (oldVersion < ... && !columnExists(...)) db.execSQL("ALTER TABLE ...") to add/modify columns as required. --- src/org/fdroid/fdroid/data/DBHelper.java | 29 +++++++++++++++--------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index 9335fe654..b7d084646 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -87,7 +87,7 @@ public class DBHelper extends SQLiteOpenHelper { + "iconUrl text, " + "primary key(id));"; - private static final int DB_VERSION = 41; + private static final int DB_VERSION = 42; private Context context; @@ -231,12 +231,12 @@ public class DBHelper extends SQLiteOpenHelper { Log.i("FDroid", "Upgrading database from v" + oldVersion + " v" + newVersion); - migradeRepoTable(db, oldVersion); + migrateRepoTable(db, oldVersion); // The other tables are transient and can just be reset. Do this after // the repo table changes though, because it also clears the lastetag // fields which didn't always exist. - resetTransient(db); + resetTransient(db, oldVersion); addNameAndDescriptionToRepo(db, oldVersion); addFingerprintToRepo(db, oldVersion); @@ -251,7 +251,7 @@ public class DBHelper extends SQLiteOpenHelper { * Migrate repo list to new structure. (No way to change primary * key in sqlite - table must be recreated). */ - private void migradeRepoTable(SQLiteDatabase db, int oldVersion) { + private void migrateRepoTable(SQLiteDatabase db, int oldVersion) { if (oldVersion < 20) { List oldrepos = new ArrayList(); Cursor c = db.query(TABLE_REPO, @@ -355,13 +355,20 @@ public class DBHelper extends SQLiteOpenHelper { } } - private void resetTransient(SQLiteDatabase db) { - context.getSharedPreferences("FDroid", Context.MODE_PRIVATE).edit() - .putBoolean("triedEmptyUpdate", false).commit(); - db.execSQL("drop table " + TABLE_APP); - db.execSQL("drop table " + TABLE_APK); - db.execSQL("update " + TABLE_REPO + " set lastetag = NULL"); - createAppApk(db); + private void resetTransient(SQLiteDatabase db, int oldVersion) { + // Before version 42, only transient info was stored in here. As of some time + // just before 42 (F-Droid 0.60ish) it now has "ignore this version" info which + // was is specified by the user. We don't want to weely-neely nuke that data. + // and the new way to deal with changes to the table structure is to add a + // if (oldVersion < x && !columnExists(...) and then alter the table as required. + if (oldVersion < 42) { + context.getSharedPreferences("FDroid", Context.MODE_PRIVATE).edit() + .putBoolean("triedEmptyUpdate", false).commit(); + db.execSQL("drop table " + TABLE_APP); + db.execSQL("drop table " + TABLE_APK); + db.execSQL("update " + TABLE_REPO + " set lastetag = NULL"); + createAppApk(db); + } } private static void createAppApk(SQLiteDatabase db) { From 94300592d9606daff8315ed5bfa3daa99f5b8be9 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 19 Mar 2014 07:02:00 +1100 Subject: [PATCH 199/282] Fix missing resource issue. When adding "default_repo_priority", it was copy/pasted from "default_repo_pubkey" without changing the name, and so tried to cast the string value into an int and failed. --- src/org/fdroid/fdroid/data/DBHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index b7d084646..3e41c6139 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -215,7 +215,7 @@ public class DBHelper extends SQLiteOpenHelper { "org.fdroid.fdroid" ))); values.put("priority", ress.getInteger(ress.getIdentifier( - "default_repo_pubkey" + i, + "default_repo_priority" + i, "integer", "org.fdroid.fdroid" ))); From ddb1cfd65978c9b918f73173f4d4e6f1e5c140c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 19 Mar 2014 12:46:17 +0100 Subject: [PATCH 200/282] Only set NOT_UNKNOWN_SOURCE if available --- src/org/fdroid/fdroid/AppDetails.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 3ecc8f4fe..1e311c6b0 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -33,6 +33,7 @@ import android.app.AlertDialog; import android.app.ListActivity; import android.app.ProgressDialog; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -913,7 +914,9 @@ public class AppDetails extends ListActivity { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(Uri.parse("file://" + file.getPath()), "application/vnd.android.package-archive"); - intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); + if (Build.VERSION.SDK_INT >= 14) { + intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); + } startActivityForResult(intent, REQUEST_INSTALL); ((FDroidApp) getApplication()).invalidateApp(id); } From 955087d523d23936de521d40d6adcc7686efb813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 19 Mar 2014 13:39:20 +0100 Subject: [PATCH 201/282] Relese 0.61-test --- AndroidManifest.xml | 4 ++-- res/values/no_trans.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 0c47514ba..3cd519cc4 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="610" + android:versionName="0.61-test" > F-Droid - 0.60-test + 0.61-test https://f-droid.org team@f-droid.org From 7dd216d212273a84dd3a46dca23411b4e46d8af6 Mon Sep 17 00:00:00 2001 From: Daniel McCarney Date: Wed, 19 Mar 2014 18:56:42 -0400 Subject: [PATCH 202/282] Apply Google CSRNG fixes. The cryptographically secure random number generator exposed to Android through the Java Cryptography Architecture is not properly initialized on some older unpatched versions of Android. Google provides a PRNGFixes.java class to force secure seeding of the CSRNG on all platform versions. This comment adds the PRNGFixes class & and a call to invoke the fixes from the FDroidApp class. More detail is available from the Google Android Developers blogpost on the subject: http://android-developers.blogspot.ca/2013/08/some-securerandom-thoughts.html --- src/org/fdroid/fdroid/FDroidApp.java | 4 + src/org/fdroid/fdroid/compat/PRNGFixes.java | 336 ++++++++++++++++++++ 2 files changed, 340 insertions(+) create mode 100644 src/org/fdroid/fdroid/compat/PRNGFixes.java diff --git a/src/org/fdroid/fdroid/FDroidApp.java b/src/org/fdroid/fdroid/FDroidApp.java index f51978b9c..a5d30acf7 100644 --- a/src/org/fdroid/fdroid/FDroidApp.java +++ b/src/org/fdroid/fdroid/FDroidApp.java @@ -50,6 +50,7 @@ import com.nostra13.universalimageloader.utils.StorageUtils; import de.duenndns.ssl.MemorizingTrustManager; import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.compat.PRNGFixes; import org.thoughtcrime.ssl.pinning.PinningTrustManager; import org.thoughtcrime.ssl.pinning.SystemKeyStore; @@ -87,6 +88,9 @@ public class FDroidApp extends Application { // it is more deterministic as to when this gets called... Preferences.setup(this); + //Apply the Google PRNG fixes to properly seed SecureRandom + PRNGFixes.apply(); + // Set this up here, and the testing framework will override it when // it gets fired up. Utils.setupInstalledApkCache(new Utils.InstalledApkCache()); diff --git a/src/org/fdroid/fdroid/compat/PRNGFixes.java b/src/org/fdroid/fdroid/compat/PRNGFixes.java new file mode 100644 index 000000000..67bd1af94 --- /dev/null +++ b/src/org/fdroid/fdroid/compat/PRNGFixes.java @@ -0,0 +1,336 @@ +package org.fdroid.fdroid.compat; + +/* + * This software is provided 'as-is', without any express or implied + * warranty. In no event will Google be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, as long as the origin is not misrepresented. + */ + +import android.os.Build; +import android.os.Process; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.SecureRandomSpi; +import java.security.Security; + +/** + * Fixes for the output of the default PRNG having low entropy. + * + * The fixes need to be applied via {@link #apply()} before any use of Java + * Cryptography Architecture primitives. A good place to invoke them is in the + * application's {@code onCreate}. + */ +public final class PRNGFixes { + + private static final int VERSION_CODE_JELLY_BEAN = 16; + private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; + private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = + getBuildFingerprintAndDeviceSerial(); + + /** Hidden constructor to prevent instantiation. */ + private PRNGFixes() {} + + /** + * Applies all fixes. + * + * @throws SecurityException if a fix is needed but could not be applied. + */ + public static void apply() { + applyOpenSSLFix(); + installLinuxPRNGSecureRandom(); + } + + /** + * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the + * fix is not needed. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void applyOpenSSLFix() throws SecurityException { + if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) + || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { + // No need to apply the fix + return; + } + + try { + // Mix in the device- and invocation-specific seed. + Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_seed", byte[].class) + .invoke(null, generateSeed()); + + // Mix output of Linux PRNG into OpenSSL's PRNG + int bytesRead = (Integer) Class.forName( + "org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_load_file", String.class, long.class) + .invoke(null, "/dev/urandom", 1024); + if (bytesRead != 1024) { + throw new IOException( + "Unexpected number of bytes read from Linux PRNG: " + + bytesRead); + } + } catch (Exception e) { + throw new SecurityException("Failed to seed OpenSSL PRNG", e); + } + } + + /** + * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the + * default. Does nothing if the implementation is already the default or if + * there is not need to install the implementation. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void installLinuxPRNGSecureRandom() + throws SecurityException { + if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { + // No need to apply the fix + return; + } + + // Install a Linux PRNG-based SecureRandom implementation as the + // default, if not yet installed. + Provider[] secureRandomProviders = + Security.getProviders("SecureRandom.SHA1PRNG"); + if ((secureRandomProviders == null) + || (secureRandomProviders.length < 1) + || (!LinuxPRNGSecureRandomProvider.class.equals( + secureRandomProviders[0].getClass()))) { + Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); + } + + // Assert that new SecureRandom() and + // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed + // by the Linux PRNG-based SecureRandom implementation. + SecureRandom rng1 = new SecureRandom(); + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng1.getProvider().getClass())) { + throw new SecurityException( + "new SecureRandom() backed by wrong Provider: " + + rng1.getProvider().getClass()); + } + + SecureRandom rng2; + try { + rng2 = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("SHA1PRNG not available", e); + } + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng2.getProvider().getClass())) { + throw new SecurityException( + "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" + + " Provider: " + rng2.getProvider().getClass()); + } + } + + /** + * {@code Provider} of {@code SecureRandom} engines which pass through + * all requests to the Linux PRNG. + */ + private static class LinuxPRNGSecureRandomProvider extends Provider { + + public LinuxPRNGSecureRandomProvider() { + super("LinuxPRNG", + 1.0, + "A Linux-specific random number provider that uses" + + " /dev/urandom"); + // Although /dev/urandom is not a SHA-1 PRNG, some apps + // explicitly request a SHA1PRNG SecureRandom and we thus need to + // prevent them from getting the default implementation whose output + // may have low entropy. + put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); + put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); + } + } + + /** + * {@link SecureRandomSpi} which passes all requests to the Linux PRNG + * ({@code /dev/urandom}). + */ + public static class LinuxPRNGSecureRandom extends SecureRandomSpi { + + /* + * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed + * are passed through to the Linux PRNG (/dev/urandom). Instances of + * this class seed themselves by mixing in the current time, PID, UID, + * build fingerprint, and hardware serial number (where available) into + * Linux PRNG. + * + * Concurrency: Read requests to the underlying Linux PRNG are + * serialized (on sLock) to ensure that multiple threads do not get + * duplicated PRNG output. + */ + + private static final File URANDOM_FILE = new File("/dev/urandom"); + + private static final Object sLock = new Object(); + + /** + * Input stream for reading from Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static DataInputStream sUrandomIn; + + /** + * Output stream for writing to Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static OutputStream sUrandomOut; + + /** + * Whether this engine instance has been seeded. This is needed because + * each instance needs to seed itself if the client does not explicitly + * seed it. + */ + private boolean mSeeded; + + @Override + protected void engineSetSeed(byte[] bytes) { + try { + OutputStream out; + synchronized (sLock) { + out = getUrandomOutputStream(); + } + out.write(bytes); + out.flush(); + } catch (IOException e) { + // On a small fraction of devices /dev/urandom is not writable. + // Log and ignore. + Log.w(PRNGFixes.class.getSimpleName(), + "Failed to mix seed into " + URANDOM_FILE); + } finally { + mSeeded = true; + } + } + + @Override + protected void engineNextBytes(byte[] bytes) { + if (!mSeeded) { + // Mix in the device- and invocation-specific seed. + engineSetSeed(generateSeed()); + } + + try { + DataInputStream in; + synchronized (sLock) { + in = getUrandomInputStream(); + } + synchronized (in) { + in.readFully(bytes); + } + } catch (IOException e) { + throw new SecurityException( + "Failed to read from " + URANDOM_FILE, e); + } + } + + @Override + protected byte[] engineGenerateSeed(int size) { + byte[] seed = new byte[size]; + engineNextBytes(seed); + return seed; + } + + private DataInputStream getUrandomInputStream() { + synchronized (sLock) { + if (sUrandomIn == null) { + // NOTE: Consider inserting a BufferedInputStream between + // DataInputStream and FileInputStream if you need higher + // PRNG output performance and can live with future PRNG + // output being pulled into this process prematurely. + try { + sUrandomIn = new DataInputStream( + new FileInputStream(URANDOM_FILE)); + } catch (IOException e) { + throw new SecurityException("Failed to open " + + URANDOM_FILE + " for reading", e); + } + } + return sUrandomIn; + } + } + + private OutputStream getUrandomOutputStream() throws IOException { + synchronized (sLock) { + if (sUrandomOut == null) { + sUrandomOut = new FileOutputStream(URANDOM_FILE); + } + return sUrandomOut; + } + } + } + + /** + * Generates a device- and invocation-specific seed to be mixed into the + * Linux PRNG. + */ + private static byte[] generateSeed() { + try { + ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); + DataOutputStream seedBufferOut = + new DataOutputStream(seedBuffer); + seedBufferOut.writeLong(System.currentTimeMillis()); + seedBufferOut.writeLong(System.nanoTime()); + seedBufferOut.writeInt(Process.myPid()); + seedBufferOut.writeInt(Process.myUid()); + seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); + seedBufferOut.close(); + return seedBuffer.toByteArray(); + } catch (IOException e) { + throw new SecurityException("Failed to generate seed", e); + } + } + + /** + * Gets the hardware serial number of this device. + * + * @return serial number or {@code null} if not available. + */ + private static String getDeviceSerialNumber() { + // We're using the Reflection API because Build.SERIAL is only available + // since API Level 9 (Gingerbread, Android 2.3). + try { + return (String) Build.class.getField("SERIAL").get(null); + } catch (Exception ignored) { + return null; + } + } + + private static byte[] getBuildFingerprintAndDeviceSerial() { + StringBuilder result = new StringBuilder(); + String fingerprint = Build.FINGERPRINT; + if (fingerprint != null) { + result.append(fingerprint); + } + String serial = getDeviceSerialNumber(); + if (serial != null) { + result.append(serial); + } + try { + return result.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported"); + } + } +} \ No newline at end of file From ded9b146a268b2fed3cc930a0cbcb609eac205ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 20 Mar 2014 00:09:17 +0100 Subject: [PATCH 203/282] Update UIL --- extern/UniversalImageLoader | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/UniversalImageLoader b/extern/UniversalImageLoader index 69aabb4f7..b1b49e51f 160000 --- a/extern/UniversalImageLoader +++ b/extern/UniversalImageLoader @@ -1 +1 @@ -Subproject commit 69aabb4f7b1834c09106c9983f99b648fab65791 +Subproject commit b1b49e51f2c43b119edca44691daf9ab6c751158 From 0e47ac690053a8fddc2b1e3d75557f63629610c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 21 Mar 2014 18:18:12 +0100 Subject: [PATCH 204/282] Slightly speed up getAndroidVersionName by using a static array --- src/org/fdroid/fdroid/Utils.java | 48 +++++++++++++++++--------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/org/fdroid/fdroid/Utils.java b/src/org/fdroid/fdroid/Utils.java index 6f2950aac..b5fa3a37b 100644 --- a/src/org/fdroid/fdroid/Utils.java +++ b/src/org/fdroid/fdroid/Utils.java @@ -118,30 +118,32 @@ public final class Utils { return String.format(FRIENDLY_SIZE_FORMAT[i], s); } + private static final String[] androidVersionNames = { + "?", // 0, undefined + "1.0", // 1 + "1.1", // 2 + "1.5", // 3 + "1.6", // 4 + "2.0", // 5 + "2.0.1", // 6 + "2.1", // 7 + "2.2", // 8 + "2.3", // 9 + "2.3.3", // 10 + "3.0", // 11 + "3.1", // 12 + "3.2", // 13 + "4.0", // 14 + "4.0.3", // 15 + "4.1", // 16 + "4.2", // 17 + "4.3", // 18 + "4.4" // 19 + }; + public static String getAndroidVersionName(int sdkLevel) { - if (sdkLevel < 1) return null; - switch (sdkLevel) { - case 19: return "4.4"; - case 18: return "4.3"; - case 17: return "4.2"; - case 16: return "4.1"; - case 15: return "4.0.3"; - case 14: return "4.0"; - case 13: return "3.2"; - case 12: return "3.1"; - case 11: return "3.0"; - case 10: return "2.3.3"; - case 9: return "2.3"; - case 8: return "2.2"; - case 7: return "2.1"; - case 6: return "2.0.1"; - case 5: return "2.0"; - case 4: return "1.6"; - case 3: return "1.5"; - case 2: return "1.1"; - case 1: return "1.0"; - default: return "?"; - } + if (sdkLevel < 0 || sdkLevel > 19) return androidVersionNames[0]; + return androidVersionNames[sdkLevel]; } public static int countSubstringOccurrence(File file, String substring) throws IOException { From 9c9ecc5140e6f1784eb63fff8b359337c8cd6f33 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Sat, 22 Mar 2014 08:25:26 +1100 Subject: [PATCH 205/282] Fixed issue 472, NPE on android 3.1 (and 3.0). The Activity.getActionBar() method can only be called after setContentView() has been invoked, as described here: http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html I couldn't think of any way to enforce this safely (i.e. make the compiler kick up a stink if we didn't do it). As such, I just put a comment above each usage of the ActionBarCompat class. Another outstanding issue is a duplicate of 474, where it crashes when you press the "Up" button from ManageRepos, but I'll create a different issue for that. --- src/org/fdroid/fdroid/AppDetails.java | 8 +++++--- src/org/fdroid/fdroid/ManageRepo.java | 9 +++++++++ src/org/fdroid/fdroid/PreferencesActivity.java | 5 +++++ src/org/fdroid/fdroid/SearchResults.java | 7 ++++++- src/org/fdroid/fdroid/compat/ActionBarCompat.java | 9 +++++++++ src/org/fdroid/fdroid/views/RepoDetailsActivity.java | 9 +++++++++ 6 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 1e311c6b0..206dfd39f 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -261,11 +261,13 @@ public class AppDetails extends ListActivity { .bitmapConfig(Bitmap.Config.RGB_565) .build(); - ActionBarCompat abCompat = ActionBarCompat.create(this); - abCompat.setDisplayHomeAsUpEnabled(true); - setContentView(R.layout.appdetails); + // Actionbar cannot be accessed until after setContentView (on 3.0 and 3.1 devices) + // see: http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html + // for reason why. + ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); + Intent i = getIntent(); Uri data = i.getData(); if (data != null) { diff --git a/src/org/fdroid/fdroid/ManageRepo.java b/src/org/fdroid/fdroid/ManageRepo.java index 102a8faa3..665f8bd6c 100644 --- a/src/org/fdroid/fdroid/ManageRepo.java +++ b/src/org/fdroid/fdroid/ManageRepo.java @@ -27,6 +27,7 @@ import android.support.v4.app.NavUtils; import android.util.Log; import android.view.MenuItem; +import android.widget.LinearLayout; import org.fdroid.fdroid.compat.ActionBarCompat; import org.fdroid.fdroid.views.fragments.RepoListFragment; @@ -49,6 +50,14 @@ public class ManageRepo extends FragmentActivity { ((FDroidApp) getApplication()).applyTheme(this); if (savedInstanceState == null) { + + // Need to set a dummy view (which will get overridden by the fragment manager + // below) so that we can call setContentView(). This is a work around for + // a (bug?) thing in 3.0, 3.1 which requires setContentView to be invoked before + // the actionbar is played with: + // http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html + setContentView( new LinearLayout(this) ); + listFragment = new RepoListFragment(); getSupportFragmentManager() .beginTransaction() diff --git a/src/org/fdroid/fdroid/PreferencesActivity.java b/src/org/fdroid/fdroid/PreferencesActivity.java index 569f3dc73..12ac901bd 100644 --- a/src/org/fdroid/fdroid/PreferencesActivity.java +++ b/src/org/fdroid/fdroid/PreferencesActivity.java @@ -59,7 +59,12 @@ public class PreferencesActivity extends PreferenceActivity implements protected void onCreate(Bundle savedInstanceState) { ((FDroidApp) getApplication()).applyTheme(this); super.onCreate(savedInstanceState); + + // Actionbar cannot be accessed until after setContentView (on 3.0 and 3.1 devices) + // see: http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html + // for reason why. ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); + addPreferencesFromResource(R.xml.preferences); } diff --git a/src/org/fdroid/fdroid/SearchResults.java b/src/org/fdroid/fdroid/SearchResults.java index 426e3f76d..f2a1d5357 100644 --- a/src/org/fdroid/fdroid/SearchResults.java +++ b/src/org/fdroid/fdroid/SearchResults.java @@ -73,9 +73,14 @@ public class SearchResults extends ListActivity { ((FDroidApp) getApplication()).applyTheme(this); super.onCreate(savedInstanceState); - ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.searchresults); + // Actionbar cannot be accessed until after setContentView (on 3.0 and 3.1 devices) + // see: http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html + // for reason why. + ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); + // Start a search by just typing setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); } diff --git a/src/org/fdroid/fdroid/compat/ActionBarCompat.java b/src/org/fdroid/fdroid/compat/ActionBarCompat.java index 52e077673..9f78c0822 100644 --- a/src/org/fdroid/fdroid/compat/ActionBarCompat.java +++ b/src/org/fdroid/fdroid/compat/ActionBarCompat.java @@ -20,6 +20,15 @@ public abstract class ActionBarCompat extends Compatibility { this.activity = activity; } + /** + * Cannot be accessed until after setContentView (on 3.0 and 3.1 devices) has + * been called on the relevant activity. If you don't have a content view + * (e.g. when using fragment manager to add fragments to the activity) then you + * will still need to call setContentView(), with a "new LinearLayout()" or something + * useless like that. + * See: http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html + * for details. + */ public abstract void setDisplayHomeAsUpEnabled(boolean value); } diff --git a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java index 533c08242..268557372 100644 --- a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -15,6 +15,7 @@ import android.os.Parcelable; import android.support.v4.app.FragmentActivity; import android.text.TextUtils; import android.util.Log; +import android.widget.LinearLayout; import android.widget.Toast; import org.fdroid.fdroid.FDroidApp; @@ -41,6 +42,14 @@ public class RepoDetailsActivity extends FragmentActivity { long repoId = getIntent().getLongExtra(RepoDetailsFragment.ARG_REPO_ID, 0); if (savedInstanceState == null) { + + // Need to set a dummy view (which will get overridden by the fragment manager + // below) so that we can call setContentView(). This is a work around for + // a (bug?) thing in 3.0, 3.1 which requires setContentView to be invoked before + // the actionbar is played with: + // http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html + setContentView( new LinearLayout(this) ); + RepoDetailsFragment fragment = new RepoDetailsFragment(); fragment.setArguments(getIntent().getExtras()); getSupportFragmentManager() From fc4a96acd85340581a7c35e4940fab75ad4f1324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 21 Mar 2014 23:22:23 +0100 Subject: [PATCH 206/282] Don't break when updating the Apk table on devices before 3.0 --- .../fdroid/fdroid/data/FDroidProvider.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/data/FDroidProvider.java b/src/org/fdroid/fdroid/data/FDroidProvider.java index e056eef3e..7ae36ff51 100644 --- a/src/org/fdroid/fdroid/data/FDroidProvider.java +++ b/src/org/fdroid/fdroid/data/FDroidProvider.java @@ -4,8 +4,12 @@ import android.annotation.TargetApi; import android.content.*; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; +import android.os.Build; import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.Map.Entry; public abstract class FDroidProvider extends ContentProvider { @@ -98,9 +102,24 @@ public abstract class FDroidProvider extends ContentProvider { } @TargetApi(11) + protected Set getKeySet(ContentValues values) { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + return values.keySet(); + } + + Set keySet = new HashSet(); + for (Entry item : values.valueSet()) { + String key = item.getKey(); + keySet.add(key); + } + return keySet; + + } + protected void validateFields(String[] validFields, ContentValues values) throws IllegalArgumentException { - for (String key : values.keySet()) { + for (String key : getKeySet(values)) { boolean isValid = false; for (String validKey : validFields) { if (validKey.equals(key)) { From 4f065492effc346180dd22c7edfa28f3054eb451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 22 Mar 2014 00:11:56 +0100 Subject: [PATCH 207/282] Unify the usage of cursors Safer and less error-prone because: * Always checks for null * Checks for sizes * Inits App/Apk lists at known capacity * Properly closes all cursors There are still one or two cursors that are not closed correctly and show things like these: W/CursorWrapperInner(19973): Cursor finalized without prior close() --- src/org/fdroid/fdroid/UpdateService.java | 13 ++-- src/org/fdroid/fdroid/data/ApkProvider.java | 26 ++++--- src/org/fdroid/fdroid/data/AppProvider.java | 43 +++++++----- src/org/fdroid/fdroid/data/DBHelper.java | 73 +++++++++++--------- src/org/fdroid/fdroid/data/RepoProvider.java | 16 +++-- 5 files changed, 102 insertions(+), 69 deletions(-) diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index f22b93281..323ab88fc 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -539,12 +539,15 @@ public class UpdateService extends IntentService implements ProgressListener { int knownIdCount = cursor != null ? cursor.getCount() : 0; List knownIds = new ArrayList(knownIdCount); - if (knownIdCount > 0) { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - knownIds.add(cursor.getString(0)); - cursor.moveToNext(); + if (cursor != null) { + if (knownIdCount > 0) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + knownIds.add(cursor.getString(0)); + cursor.moveToNext(); + } } + cursor.close(); } return knownIds; diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java index e607a53f4..9ecff515a 100644 --- a/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -32,12 +32,15 @@ public class ApkProvider extends FDroidProvider { } public static List cursorToList(Cursor cursor) { - List apks = new ArrayList(); + int knownApkCount = cursor != null ? cursor.getCount() : 0; + List apks = new ArrayList(knownApkCount); if (cursor != null) { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - apks.add(new Apk(cursor)); - cursor.moveToNext(); + if (knownApkCount > 0) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + apks.add(new Apk(cursor)); + cursor.moveToNext(); + } } cursor.close(); } @@ -64,12 +67,15 @@ public class ApkProvider extends FDroidProvider { ContentResolver resolver = context.getContentResolver(); Uri uri = getContentUri(id, versionCode); Cursor cursor = resolver.query(uri, projection, null, null, null); - if (cursor != null && cursor.getCount() > 0) { - cursor.moveToFirst(); - return new Apk(cursor); - } else { - return null; + Apk apk = null; + if (cursor != null) { + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + apk = new Apk(cursor); + } + cursor.close(); } + return apk; } public static List findByApp(Context context, String appId) { diff --git a/src/org/fdroid/fdroid/data/AppProvider.java b/src/org/fdroid/fdroid/data/AppProvider.java index 463b57317..95176fcdb 100644 --- a/src/org/fdroid/fdroid/data/AppProvider.java +++ b/src/org/fdroid/fdroid/data/AppProvider.java @@ -40,12 +40,15 @@ public class AppProvider extends FDroidProvider { } private static List cursorToList(Cursor cursor) { - List apps = new ArrayList(); + int knownAppCount = cursor != null ? cursor.getCount() : 0; + List apps = new ArrayList(knownAppCount); if (cursor != null) { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - apps.add(new App(cursor)); - cursor.moveToNext(); + if (knownAppCount > 0) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + apps.add(new App(cursor)); + cursor.moveToNext(); + } } cursor.close(); } @@ -71,16 +74,19 @@ public class AppProvider extends FDroidProvider { Cursor cursor = resolver.query(uri, projection, null, null, null ); Set categorySet = new HashSet(); if (cursor != null) { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - String categoriesString = cursor.getString(0); - if (categoriesString != null) { - for( String s : Utils.CommaSeparatedList.make(categoriesString)) { - categorySet.add(s); + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + String categoriesString = cursor.getString(0); + if (categoriesString != null) { + for (String s : Utils.CommaSeparatedList.make(categoriesString)) { + categorySet.add(s); + } } + cursor.moveToNext(); } - cursor.moveToNext(); } + cursor.close(); } List categories = new ArrayList(categorySet); Collections.sort(categories); @@ -103,12 +109,15 @@ public class AppProvider extends FDroidProvider { String[] projection) { Uri uri = getContentUri(appId); Cursor cursor = resolver.query(uri, projection, null, null, null); - if (cursor != null && cursor.getCount() > 0) { - cursor.moveToFirst(); - return new App(cursor); - } else { - return null; + App app = null; + if (cursor != null) { + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + app = new App(cursor); + } + cursor.close(); } + return app; } public static void deleteAppsWithNoApks(ContentResolver resolver) { diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index 3e41c6139..feb323d91 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -102,19 +102,22 @@ public class DBHelper extends SQLiteOpenHelper { String[] columns = { "address", "_id" }; Cursor cursor = db.query(TABLE_REPO, columns, "name IS NULL OR name = ''", null, null, null, null); - if (cursor.getCount() > 0) { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - String address = cursor.getString(0); - long id = cursor.getInt(1); - ContentValues values = new ContentValues(1); - String name = Repo.addressToName(address); - values.put("name", name); - String[] args = { Long.toString( id ) }; - Log.i("FDroid", "Setting repo name to '" + name + "' for repo " + address); - db.update(TABLE_REPO, values, "_id = ?", args); - cursor.moveToNext(); + if (cursor != null) { + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + String address = cursor.getString(0); + long id = cursor.getInt(1); + ContentValues values = new ContentValues(1); + String name = Repo.addressToName(address); + values.put("name", name); + String[] args = { Long.toString( id ) }; + Log.i("FDroid", "Setting repo name to '" + name + "' for repo " + address); + db.update(TABLE_REPO, values, "_id = ?", args); + cursor.moveToNext(); + } } + cursor.close(); } } } @@ -254,19 +257,23 @@ public class DBHelper extends SQLiteOpenHelper { private void migrateRepoTable(SQLiteDatabase db, int oldVersion) { if (oldVersion < 20) { List oldrepos = new ArrayList(); - Cursor c = db.query(TABLE_REPO, + Cursor cursor = db.query(TABLE_REPO, new String[] { "address", "inuse", "pubkey" }, null, null, null, null, null); - c.moveToFirst(); - while (!c.isAfterLast()) { - Repo repo = new Repo(); - repo.address = c.getString(0); - repo.inuse = (c.getInt(1) == 1); - repo.pubkey = c.getString(2); - oldrepos.add(repo); - c.moveToNext(); + if (cursor != null) { + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + Repo repo = new Repo(); + repo.address = cursor.getString(0); + repo.inuse = (cursor.getInt(1) == 1); + repo.pubkey = cursor.getString(2); + oldrepos.add(repo); + cursor.moveToNext(); + } + } + cursor.close(); } - c.close(); db.execSQL("drop table " + TABLE_REPO); db.execSQL(CREATE_TABLE_REPO); for (Repo repo : oldrepos) { @@ -316,18 +323,22 @@ public class DBHelper extends SQLiteOpenHelper { if (!columnExists(db, TABLE_REPO, "fingerprint")) db.execSQL("alter table " + TABLE_REPO + " add column fingerprint text"); List oldrepos = new ArrayList(); - Cursor c = db.query(TABLE_REPO, + Cursor cursor = db.query(TABLE_REPO, new String[] { "address", "pubkey" }, null, null, null, null, null); - c.moveToFirst(); - while (!c.isAfterLast()) { - Repo repo = new Repo(); - repo.address = c.getString(0); - repo.pubkey = c.getString(1); - oldrepos.add(repo); - c.moveToNext(); + if (cursor != null) { + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + Repo repo = new Repo(); + repo.address = cursor.getString(0); + repo.pubkey = cursor.getString(1); + oldrepos.add(repo); + cursor.moveToNext(); + } + } + cursor.close(); } - c.close(); for (Repo repo : oldrepos) { ContentValues values = new ContentValues(); values.put("fingerprint", Utils.calcFingerprint(repo.pubkey)); diff --git a/src/org/fdroid/fdroid/data/RepoProvider.java b/src/org/fdroid/fdroid/data/RepoProvider.java index 77e567a4d..5be9fd44b 100644 --- a/src/org/fdroid/fdroid/data/RepoProvider.java +++ b/src/org/fdroid/fdroid/data/RepoProvider.java @@ -33,6 +33,7 @@ public class RepoProvider extends FDroidProvider { if (cursor != null) { cursor.moveToFirst(); repo = new Repo(cursor); + cursor.close(); } return repo; } @@ -176,13 +177,16 @@ public class RepoProvider extends FDroidProvider { ContentResolver resolver = context.getContentResolver(); String[] projection = { ApkProvider.DataColumns._COUNT_DISTINCT_ID }; Uri apkUri = ApkProvider.getRepoUri(repoId); - Cursor result = resolver.query(apkUri, projection, null, null, null); - if (result != null && result.getCount() > 0) { - result.moveToFirst(); - return result.getInt(0); - } else { - return 0; + Cursor cursor = resolver.query(apkUri, projection, null, null, null); + int count = 0; + if (cursor != null) { + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + count = cursor.getInt(0); + } + cursor.close(); } + return count; } } From a1a8c06565ed754437fbad1cb8c5b0af2dd51755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 22 Mar 2014 11:12:42 +0100 Subject: [PATCH 208/282] Start using contentDescription on ImageView elements --- res/layout-land/appdetails.xml | 4 +++- res/layout/appdetails.xml | 10 +++++++--- res/layout/applistitem.xml | 7 +++++-- res/layout/repo_item.xml | 7 +++++-- res/layout/repolisticons.xml | 9 ++++++--- res/values/strings.xml | 3 +++ 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/res/layout-land/appdetails.xml b/res/layout-land/appdetails.xml index 1aba209ed..9f91b96ff 100644 --- a/res/layout-land/appdetails.xml +++ b/res/layout-land/appdetails.xml @@ -36,10 +36,12 @@ + android:scaleType="fitCenter" + /> + android:scaleType="fitCenter" + /> + android:orientation="vertical" + > + android:textSize="12sp" + /> - + android:scaleType="fitCenter" + /> - + android:layout_alignParentStart="true" + /> - + android:layout_height="wrap_content" + /> \ No newline at end of file +--> diff --git a/res/values/strings.xml b/res/values/strings.xml index 3ff4a69d4..6f15f3a5e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -213,4 +213,7 @@ Discovered Repo Name Repo Address + App icon + Repo icon + From 2cd17a904e7db9e6c400c44b036409d21916fd40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 22 Mar 2014 11:18:42 +0100 Subject: [PATCH 209/282] Placed the TargetApis wrong --- src/org/fdroid/fdroid/NfcNotEnabledActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/fdroid/fdroid/NfcNotEnabledActivity.java b/src/org/fdroid/fdroid/NfcNotEnabledActivity.java index 793949cc7..bdca0542c 100644 --- a/src/org/fdroid/fdroid/NfcNotEnabledActivity.java +++ b/src/org/fdroid/fdroid/NfcNotEnabledActivity.java @@ -19,7 +19,7 @@ public class NfcNotEnabledActivity extends Activity { * needed for NDEF. Therefore, we detect the current state of NFC, * and steer the user accordingly. */ - @TargetApi(14) + @TargetApi(16) private void doOnJellybean(Intent intent) { if (NfcAdapter.getDefaultAdapter(this).isEnabled()) intent.setAction(Settings.ACTION_NFCSHARING_SETTINGS); @@ -28,7 +28,7 @@ public class NfcNotEnabledActivity extends Activity { } // this API was added in 4.0 aka Ice Cream Sandwich - @TargetApi(16) + @TargetApi(14) private void doOnIceCreamSandwich(Intent intent) { intent.setAction(Settings.ACTION_NFCSHARING_SETTINGS); } From 9f2de0abd79b4725a291f08f2759360ed0fe6d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 22 Mar 2014 11:22:03 +0100 Subject: [PATCH 210/282] Get rid of EXTRA_NOT_UNKNOWN_SOURCE target api warning --- src/org/fdroid/fdroid/AppDetails.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 206dfd39f..87c552db0 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -29,6 +29,7 @@ import android.widget.*; import org.fdroid.fdroid.data.*; import org.xml.sax.XMLReader; +import android.annotation.TargetApi; import android.app.AlertDialog; import android.app.ListActivity; import android.app.ProgressDialog; @@ -912,13 +913,19 @@ public class AppDetails extends ListActivity { } + @TargetApi(14) + private void extraNotUnknownSource(Intent intent) { + if (Build.VERSION.SDK_INT < 14) { + return; + } + intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); + } + private void installApk(File file, String id) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(Uri.parse("file://" + file.getPath()), "application/vnd.android.package-archive"); - if (Build.VERSION.SDK_INT >= 14) { - intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); - } + extraNotUnknownSource(intent); startActivityForResult(intent, REQUEST_INSTALL); ((FDroidApp) getApplication()).invalidateApp(id); } From 361fbc83ba47a1ab6f858cc37d9b053b366dff65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 22 Mar 2014 11:24:44 +0100 Subject: [PATCH 211/282] Re-organize default repos to separate and distinguish them --- res/values/default_repo.xml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/res/values/default_repo.xml b/res/values/default_repo.xml index b6c463bea..638c93903 100644 --- a/res/values/default_repo.xml +++ b/res/values/default_repo.xml @@ -1,16 +1,18 @@ 2 + F-Droid - F-Droid Archive 1 - 0 10 - 20 https://f-droid.org/repo - https://f-droid.org/archive The official FDroid repository. Applications in this repository are mostly built directory from the source code. Some are official binaries built by the original application developers - these will be replaced by source-built versions over time. - The archive repository of the F-Droid client. This contains older versions of applications from the main repository. 3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef + + F-Droid Archive + 0 + 20 + https://f-droid.org/archive + The archive repository of the F-Droid client. This contains older versions of applications from the main repository. 3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef From ca2fe21bebf89e9a04af8b56c5e6ac791883bfee Mon Sep 17 00:00:00 2001 From: F-Droid Translatebot Date: Sat, 22 Mar 2014 12:09:05 +0000 Subject: [PATCH 212/282] Translation updates --- res/values-de/strings.xml | 28 ++++++++++++++-------------- res/values-es/strings.xml | 16 ++++++++++++++++ res/values-fr/strings.xml | 11 +++++++++++ res/values-it/strings.xml | 7 +++++++ 4 files changed, 48 insertions(+), 14 deletions(-) diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index b8d72f794..ef5b17981 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -9,7 +9,7 @@ Version Bearbeiten Löschen - Aktiviere NFC Transfer… + NFC-Transfer aktivieren … Anwendungszwischenspeicher Heruntergeladene Programmpakete auf der SD-Karte behalten Installationsdateien nicht behalten @@ -66,9 +66,9 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Anwendungsliste wird aktualisiert … Anwendung wird heruntergeladen von NFC ist deaktiviert! - Öffne NFC Einstellungen… - Keine Bluetooth Verbindung gefunden. Bitte wählen Sie eine Verbindung aus! - Wähle Bluetooth Übertragung + NFC-Einstellungen öffnen … + Keine Bluetooth-Sendemethode gefunden. Bitte wählen Sie eine aus! + Bluetooth-Sendemethode auswählen Adresse der Paketquelle Fingerabdruck (optional) Diese Paketquelle existiert bereits! @@ -76,18 +76,18 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Diese Paketquelle ist bereits eingerichtet, bestätigen, dass Sie diese wieder aktivieren möchten. Die eingehende Paketquelle ist bereits eingerichtet und aktiviert! Sie müssen diese Paketquelle zuerst löschen, bevor Sie eine mit einem anderen Schlüssel hinzuzufügen! - Ignoriere fehlerhafte Paketquellen URI: %s + Fehlerhafte Paketquellenadresse ignorieren: %s Die Liste der genutzten Paketquellen hat sich geändert. Sollen diese aktualisiert werden? Paketquellen aktualisieren Paketquellen verwalten - Bluetooth FDroid.apk… + Bluetooth-FDroid.apk … Einstellungen Über Suchen Paketquelle hinzufügen Paketquelle entfernen - Finde lokale Paketquellen + Lokale Paketquellen finden Starten Empfehlen Installieren @@ -127,8 +127,8 @@ Sollen diese aktualisiert werden? Alle Was gibt es Neues Kürzlich Aktualisiert - Lokale F-Droid Paketquellen - Entdecke lokale F-Droid Paketquellen… + Lokale F-Droid-Paketquellen + Lokale F-Droid-Paketquellen entdecken … Herunterladen %2$s / %3$s (%4$d%%) von %1$s @@ -138,7 +138,7 @@ Sollen diese aktualisiert werden? Verbinden mit %1$s Kompatibilität mit Ihrem Gerät wird überprüft … - Speichere App-Details (%1$d%%) + App-Details speichern (%1$d%%) Es werden keine Berechtigungen verwendet. Berechtigungen für Version %s Berechtigungen anzeigen @@ -152,7 +152,7 @@ Sollen diese aktualisiert werden? Nicht signiert Adresse Anwendungsanzahl - Fingerabdruck des Signaturschlüssels (SHA-256) + Fingerabdruck des Paketquellensignaturschlüssels (SHA-256) Beschreibung Letzte Aktualisierung Aktualisierung @@ -182,8 +182,8 @@ Bemerkung: Alle Sie müssen diese Paketquelle wieder aktivieren, um Anwendungen daraus installieren zu können. %s oder später - bis %s - %1$s bis %2$s - Dieses Gerät befindet sich nicht im selben WLAN wie die eben hinzugefügte Paketquelle. Versuchen Sie sich mit dem folgenden Netzwek zu verbinden: %s + bis zu %s + %1$s bis zu %2$s + Dieses Gerät befindet sich nicht im selben WLAN, wie die eben hinzugefügte Paketquelle. Versuchen Sie sich mit dem folgenden Netzwerk zu verbinden: %s Erfordert: %1$s diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 03e583dfe..a1c1fdf5c 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -9,6 +9,7 @@ Versión Editar Borrar + Habilitar envío NFC... Caché de aplicaciones descargadas Mantener en la tarjeta SD los ficheros apk descargados No conservar ningún archivo apk @@ -63,6 +64,10 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Por favor, espera Actualizando la lista de aplicaciones… Obteniendo la aplicación de + ¡No está habilitado NFC! + Ir a los ajustes de NFC... + No se encontró método de envío Bluetooth, ¡elige uno! + Elegir el método de envío Bluetooth Dirección del repositorio Huella digital (opcional) ¡Este repositorio ya existe! @@ -70,15 +75,18 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Este repositorio ya está configurado, confirma que quieres volver a habilitarlo. ¡El repositorio ya está configurado y habilitado! ¡Debes borrar este repositorio antes de añadir uno con una clave diferente! + Ignorando URI de repo mal formada: %s La lista de repositorios usada ha cambiado. ¿Deseas actualizarlos? Actualizar repositorios Gestionar Repositorios + Bluetooth FDroid.apk... Preferencias Acerca de Buscar Repositorio nuevo Borrar Repositorio + Buscar repos locales Ejecutar Compartir Instalar @@ -118,6 +126,8 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Todos Novedades Recientemente actualizados + Repos de FDroid locales + Descubriendo repos locales de FDroid... Descargando %2$s / %3$s (%4$d%%) de %1$s @@ -127,6 +137,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Conectando a %1$s Comprobando la compatibilidad de las aplicaciones con tu dispositivo… + Guardando detalles de la aplicación (%1$d%%) No se usan permisos. Permisos para la versión %s Mostrar permisos @@ -140,6 +151,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo No firmado URL Número de aplicaciones + Huella digital de la clave de firmado del repo (SHA-256) Descripción Última actualización Actualizar @@ -163,4 +175,8 @@ Nota: todas las aplicaciones previamente instaladas se quedarán en tu dispositi Necesitarás volver a habilitar este repositorio para instalar aplicaciones desde él. %s o posterior + hasta %s + De %1$ a %2$s + ¡Tu dispositivo no está en la misma WiFi que el repo que acabas de añadir! Intenta unirte a esta red: %s + Requiere: %1$s diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 12651403d..a852ae8e7 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -17,11 +17,15 @@ Autres Dernière analyse du dépôt : %s jamais + Intervalle de mise à jour automatique Seulement par WiFi + Mettre à jour automatiquement les listes d\'applications uniquement par WiFi Toujours mettre à jour automatiquement les listes d\'apps Notifier + Notifier quand des mises à jour sont disponibles Ne pas notifier des mises à jour Historique des mises à jour + Jours pour considérer les apps comme nouvelles ou récentes : %s Résultats de la recherche Détails de l\'application Pas d\'application trouvée @@ -66,6 +70,7 @@ L\'URL d\'un dépôt ressemble à ceci : https://f-droid.org/repo Adresse du dépôt Empreinte digitale (optionnel) Ce dépôt existe déja ! + Ce dépôt est déjà configuré, cela va ajouter les informations des nouvelles clés. Ce dépôt est déja configuré, confirmer que vous voulez le réactiver. Vous devez d\'abord supprimer ce dépôt avant d\'en ajouter avec une clé différente. La liste des dépôts utilisés a changé. @@ -98,14 +103,18 @@ Voulez-vous les mettre à jour ? Cette application promeut des extensions privatrices Cette application promeut des services réseaux privateurs Cette application dépend d\'autres applications non libres + Le code source en amont n\'est pas entièrement libre Affichage Expert + Afficher plus d\'infos et activer des paramètres supplémentaires Cacher des extras pour les utilisateurs avancés Rechercher des applications Compatibilité de l\'application Versions incompatibles + Cacher les applications incompatibles avec l\'appareil Root Ignorer l\'écran tactile + Toujours inclure les apps nécessitant un écran tactile Filtrer les apps normalement Tout Quoi de neuf ? @@ -124,6 +133,7 @@ Voulez-vous les mettre à jour ? Aucune autorisation n\'est utilisée. Autorisations pour la version %s Afficher les autorisations + Afficher la liste des permissions qu\'une app requiert Vous n\'avez aucune application installée pour gérer %s Affichage compact Montrer les icônes à une taille plus petite @@ -134,6 +144,7 @@ Voulez-vous les mettre à jour ? Nombre d\'applications Description Dernière mise à jour + Mise à jour Nom Voulez vous supprimer le dépôt \"{0}\", qui a {1} apps ? Toutes les applications installées ne seront pas supprimés, mais vous ne serez plus en mesure de les mettre à jour via F-Droid. Inconnu diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 20fa7ae38..4c2d22a17 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -63,6 +63,9 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo Attendere prego Aggiornamento elenco applicazioni… Scaricamento applicazione da + NFC non attivato! + Vai alle Impostazioni NFC... + Nessun metodo d\'invio via Bluetooth trovato, selezionane uno! Indirizzo repository Impronta digitale (opzionale) Questo repository esiste già! @@ -79,6 +82,7 @@ Vuoi aggiornarlo? Cerca Nuovo Repository Rimuovi Repository + Trova dei Repository locali Avvia Condividi Installa @@ -156,4 +160,7 @@ Nota: Tutte le applicazioni installate precedentemente rimarranno sul dispositiv \"%1$s\" è disabilitato. È necessario abilitare nuovamente questo repository per installare le applicazioni contenute. %s o successivi + Il tuo dispositivo non è nella stessa rete WiFi del Repository locale che hai aggiunto! +Prova a collegarti a questa rete: %s + Richiede: %1$s From a6a133b88503158edca10d435394c60a171fcd26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 22 Mar 2014 11:44:27 +0100 Subject: [PATCH 213/282] Gradle: bump versions, cleanup --- build.gradle | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/build.gradle b/build.gradle index fded3e62b..e15c643a0 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.8.+' + classpath 'com.android.tools.build:gradle:0.9.1' } } @@ -16,31 +16,21 @@ dependencies { compile project(':extern:MemorizingTrustManager') } -subprojects { +project(':extern:UniversalImageLoader:library') { buildscript { repositories { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.8.+' + classpath 'com.android.tools.build:gradle:0.9.1' } } - apply plugin: 'android-library' - android { - buildToolsVersion '19.0.2' - packagingOptions { - exclude "META-INF/LICENSE*" - exclude "META-INF/NOTICE*" - exclude "META-INF/README*" - exclude "META-INF/CHANGELOG*" - exclude "META-INF/BUILD*" - } - } -} -project(':extern:UniversalImageLoader:library') { + apply plugin: 'android' + android { compileSdkVersion 16 + buildToolsVersion '19.0.3' sourceSets { main { @@ -56,7 +46,7 @@ project(':extern:UniversalImageLoader:library') { android { compileSdkVersion 19 - buildToolsVersion '19.0.2' + buildToolsVersion '19.0.3' sourceSets { main { From 19aa5eb7f76aef9c08844605a2678e12b8c22e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 22 Mar 2014 13:20:36 +0100 Subject: [PATCH 214/282] Make fix-ellipsis remove weird spaces too --- tools/fix-ellipsis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/fix-ellipsis.sh b/tools/fix-ellipsis.sh index b3f02fb85..490027b75 100755 --- a/tools/fix-ellipsis.sh +++ b/tools/fix-ellipsis.sh @@ -2,4 +2,4 @@ # Fix TypographyEllipsis programmatically -sed -i 's/\.\.\./…/g' res/values*/*.xml +sed -i -e 's/\.\.\./…/g' -e 's/ …/…/g' res/values*/*.xml From 53ac16fceeaa38682bd7c1542cdd701e58455e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 22 Mar 2014 13:20:49 +0100 Subject: [PATCH 215/282] Run fix-ellipsis --- res/values-de/strings.xml | 12 ++++++------ res/values-es/strings.xml | 8 ++++---- res/values-it/strings.xml | 2 +- res/values-ro/strings.xml | 4 ++-- res/values-sl/strings.xml | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index ef5b17981..0636ae0c8 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -9,7 +9,7 @@ Version Bearbeiten Löschen - NFC-Transfer aktivieren … + NFC-Transfer aktivieren… Anwendungszwischenspeicher Heruntergeladene Programmpakete auf der SD-Karte behalten Installationsdateien nicht behalten @@ -63,10 +63,10 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re %d Aktualisierungen sind verfügbar. F-Droid: Aktualisierungen verfügbar Bitte warten - Anwendungsliste wird aktualisiert … + Anwendungsliste wird aktualisiert… Anwendung wird heruntergeladen von NFC ist deaktiviert! - NFC-Einstellungen öffnen … + NFC-Einstellungen öffnen… Keine Bluetooth-Sendemethode gefunden. Bitte wählen Sie eine aus! Bluetooth-Sendemethode auswählen Adresse der Paketquelle @@ -81,7 +81,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Sollen diese aktualisiert werden? Paketquellen aktualisieren Paketquellen verwalten - Bluetooth-FDroid.apk … + Bluetooth-FDroid.apk… Einstellungen Über Suchen @@ -128,7 +128,7 @@ Sollen diese aktualisiert werden? Was gibt es Neues Kürzlich Aktualisiert Lokale F-Droid-Paketquellen - Lokale F-Droid-Paketquellen entdecken … + Lokale F-Droid-Paketquellen entdecken… Herunterladen %2$s / %3$s (%4$d%%) von %1$s @@ -137,7 +137,7 @@ Sollen diese aktualisiert werden? %1$s Verbinden mit %1$s - Kompatibilität mit Ihrem Gerät wird überprüft … + Kompatibilität mit Ihrem Gerät wird überprüft… App-Details speichern (%1$d%%) Es werden keine Berechtigungen verwendet. Berechtigungen für Version %s diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index a1c1fdf5c..68486ad17 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -9,7 +9,7 @@ Versión Editar Borrar - Habilitar envío NFC... + Habilitar envío NFC… Caché de aplicaciones descargadas Mantener en la tarjeta SD los ficheros apk descargados No conservar ningún archivo apk @@ -65,7 +65,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Actualizando la lista de aplicaciones… Obteniendo la aplicación de ¡No está habilitado NFC! - Ir a los ajustes de NFC... + Ir a los ajustes de NFC… No se encontró método de envío Bluetooth, ¡elige uno! Elegir el método de envío Bluetooth Dirección del repositorio @@ -80,7 +80,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo ¿Deseas actualizarlos? Actualizar repositorios Gestionar Repositorios - Bluetooth FDroid.apk... + Bluetooth FDroid.apk… Preferencias Acerca de Buscar @@ -127,7 +127,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Novedades Recientemente actualizados Repos de FDroid locales - Descubriendo repos locales de FDroid... + Descubriendo repos locales de FDroid… Descargando %2$s / %3$s (%4$d%%) de %1$s diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 4c2d22a17..89b82eae6 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -64,7 +64,7 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo Aggiornamento elenco applicazioni… Scaricamento applicazione da NFC non attivato! - Vai alle Impostazioni NFC... + Vai alle Impostazioni NFC… Nessun metodo d\'invio via Bluetooth trovato, selezionane uno! Indirizzo repository Impronta digitale (opzionale) diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml index f0f0db7ae..67ea92024 100644 --- a/res/values-ro/strings.xml +++ b/res/values-ro/strings.xml @@ -25,6 +25,6 @@ Distribuit sub licenta GNU GPLv3. Actualizare depozit aplicatii Disponibil Actualizare - Asteptati … - Se actualizeaza lista … + Asteptati… + Se actualizeaza lista… diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index 8f98a56bb..6d83a3572 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -28,7 +28,7 @@ Izdan z licenco GNU GPLv3. Na razpolago Posodobitve Počakajte prosim - Poteka posodobitev spiska aplikacij … + Poteka posodobitev spiska aplikacij… Prejem aplikacije iz Naslov skladišča Spisek uporabljenih skladišč se je spremenil. From 7f315720abde2d1d3d7f4c18d97d2a13ea9367d1 Mon Sep 17 00:00:00 2001 From: F-Droid Translatebot Date: Sat, 22 Mar 2014 12:25:06 +0000 Subject: [PATCH 216/282] Translation updates --- res/values-de/strings.xml | 10 +++++----- res/values-es/strings.xml | 8 ++++---- res/values-it/strings.xml | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 0636ae0c8..f324dd74f 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -9,7 +9,7 @@ Version Bearbeiten Löschen - NFC-Transfer aktivieren… + NFC-Transfer aktivieren … Anwendungszwischenspeicher Heruntergeladene Programmpakete auf der SD-Karte behalten Installationsdateien nicht behalten @@ -66,7 +66,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Anwendungsliste wird aktualisiert… Anwendung wird heruntergeladen von NFC ist deaktiviert! - NFC-Einstellungen öffnen… + NFC-Einstellungen öffnen … Keine Bluetooth-Sendemethode gefunden. Bitte wählen Sie eine aus! Bluetooth-Sendemethode auswählen Adresse der Paketquelle @@ -81,7 +81,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Sollen diese aktualisiert werden? Paketquellen aktualisieren Paketquellen verwalten - Bluetooth-FDroid.apk… + Bluetooth-FDroid.apk … Einstellungen Über Suchen @@ -128,7 +128,7 @@ Sollen diese aktualisiert werden? Was gibt es Neues Kürzlich Aktualisiert Lokale F-Droid-Paketquellen - Lokale F-Droid-Paketquellen entdecken… + Lokale F-Droid-Paketquellen entdecken … Herunterladen %2$s / %3$s (%4$d%%) von %1$s @@ -137,7 +137,7 @@ Sollen diese aktualisiert werden? %1$s Verbinden mit %1$s - Kompatibilität mit Ihrem Gerät wird überprüft… + Kompatibilität mit Ihrem Gerät wird überprüft … App-Details speichern (%1$d%%) Es werden keine Berechtigungen verwendet. Berechtigungen für Version %s diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 68486ad17..a1c1fdf5c 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -9,7 +9,7 @@ Versión Editar Borrar - Habilitar envío NFC… + Habilitar envío NFC... Caché de aplicaciones descargadas Mantener en la tarjeta SD los ficheros apk descargados No conservar ningún archivo apk @@ -65,7 +65,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Actualizando la lista de aplicaciones… Obteniendo la aplicación de ¡No está habilitado NFC! - Ir a los ajustes de NFC… + Ir a los ajustes de NFC... No se encontró método de envío Bluetooth, ¡elige uno! Elegir el método de envío Bluetooth Dirección del repositorio @@ -80,7 +80,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo ¿Deseas actualizarlos? Actualizar repositorios Gestionar Repositorios - Bluetooth FDroid.apk… + Bluetooth FDroid.apk... Preferencias Acerca de Buscar @@ -127,7 +127,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Novedades Recientemente actualizados Repos de FDroid locales - Descubriendo repos locales de FDroid… + Descubriendo repos locales de FDroid... Descargando %2$s / %3$s (%4$d%%) de %1$s diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 89b82eae6..4c2d22a17 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -64,7 +64,7 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo Aggiornamento elenco applicazioni… Scaricamento applicazione da NFC non attivato! - Vai alle Impostazioni NFC… + Vai alle Impostazioni NFC... Nessun metodo d\'invio via Bluetooth trovato, selezionane uno! Indirizzo repository Impronta digitale (opzionale) From db1adb327af2bc4f66ccb57a3794a42dd2bb9be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 22 Mar 2014 13:27:16 +0100 Subject: [PATCH 217/282] Re-run fix-ellipsis --- res/values-de/strings.xml | 10 +++++----- res/values-es/strings.xml | 8 ++++---- res/values-it/strings.xml | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index f324dd74f..0636ae0c8 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -9,7 +9,7 @@ Version Bearbeiten Löschen - NFC-Transfer aktivieren … + NFC-Transfer aktivieren… Anwendungszwischenspeicher Heruntergeladene Programmpakete auf der SD-Karte behalten Installationsdateien nicht behalten @@ -66,7 +66,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Anwendungsliste wird aktualisiert… Anwendung wird heruntergeladen von NFC ist deaktiviert! - NFC-Einstellungen öffnen … + NFC-Einstellungen öffnen… Keine Bluetooth-Sendemethode gefunden. Bitte wählen Sie eine aus! Bluetooth-Sendemethode auswählen Adresse der Paketquelle @@ -81,7 +81,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Sollen diese aktualisiert werden? Paketquellen aktualisieren Paketquellen verwalten - Bluetooth-FDroid.apk … + Bluetooth-FDroid.apk… Einstellungen Über Suchen @@ -128,7 +128,7 @@ Sollen diese aktualisiert werden? Was gibt es Neues Kürzlich Aktualisiert Lokale F-Droid-Paketquellen - Lokale F-Droid-Paketquellen entdecken … + Lokale F-Droid-Paketquellen entdecken… Herunterladen %2$s / %3$s (%4$d%%) von %1$s @@ -137,7 +137,7 @@ Sollen diese aktualisiert werden? %1$s Verbinden mit %1$s - Kompatibilität mit Ihrem Gerät wird überprüft … + Kompatibilität mit Ihrem Gerät wird überprüft… App-Details speichern (%1$d%%) Es werden keine Berechtigungen verwendet. Berechtigungen für Version %s diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index a1c1fdf5c..68486ad17 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -9,7 +9,7 @@ Versión Editar Borrar - Habilitar envío NFC... + Habilitar envío NFC… Caché de aplicaciones descargadas Mantener en la tarjeta SD los ficheros apk descargados No conservar ningún archivo apk @@ -65,7 +65,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Actualizando la lista de aplicaciones… Obteniendo la aplicación de ¡No está habilitado NFC! - Ir a los ajustes de NFC... + Ir a los ajustes de NFC… No se encontró método de envío Bluetooth, ¡elige uno! Elegir el método de envío Bluetooth Dirección del repositorio @@ -80,7 +80,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo ¿Deseas actualizarlos? Actualizar repositorios Gestionar Repositorios - Bluetooth FDroid.apk... + Bluetooth FDroid.apk… Preferencias Acerca de Buscar @@ -127,7 +127,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Novedades Recientemente actualizados Repos de FDroid locales - Descubriendo repos locales de FDroid... + Descubriendo repos locales de FDroid… Descargando %2$s / %3$s (%4$d%%) de %1$s diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 4c2d22a17..89b82eae6 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -64,7 +64,7 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo Aggiornamento elenco applicazioni… Scaricamento applicazione da NFC non attivato! - Vai alle Impostazioni NFC... + Vai alle Impostazioni NFC… Nessun metodo d\'invio via Bluetooth trovato, selezionane uno! Indirizzo repository Impronta digitale (opzionale) From 3bfd5cbf0dacb37ab6c56b1fca5bc57c719ff205 Mon Sep 17 00:00:00 2001 From: F-Droid Translatebot Date: Sat, 22 Mar 2014 19:20:04 +0000 Subject: [PATCH 218/282] Translation updates --- res/values-de/strings.xml | 10 +++++----- res/values-es/strings.xml | 8 ++++---- res/values-eu/strings.xml | 36 ++++++++++++++++++++++++++++++++++++ res/values-it/strings.xml | 2 +- 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 0636ae0c8..f324dd74f 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -9,7 +9,7 @@ Version Bearbeiten Löschen - NFC-Transfer aktivieren… + NFC-Transfer aktivieren … Anwendungszwischenspeicher Heruntergeladene Programmpakete auf der SD-Karte behalten Installationsdateien nicht behalten @@ -66,7 +66,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Anwendungsliste wird aktualisiert… Anwendung wird heruntergeladen von NFC ist deaktiviert! - NFC-Einstellungen öffnen… + NFC-Einstellungen öffnen … Keine Bluetooth-Sendemethode gefunden. Bitte wählen Sie eine aus! Bluetooth-Sendemethode auswählen Adresse der Paketquelle @@ -81,7 +81,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Sollen diese aktualisiert werden? Paketquellen aktualisieren Paketquellen verwalten - Bluetooth-FDroid.apk… + Bluetooth-FDroid.apk … Einstellungen Über Suchen @@ -128,7 +128,7 @@ Sollen diese aktualisiert werden? Was gibt es Neues Kürzlich Aktualisiert Lokale F-Droid-Paketquellen - Lokale F-Droid-Paketquellen entdecken… + Lokale F-Droid-Paketquellen entdecken … Herunterladen %2$s / %3$s (%4$d%%) von %1$s @@ -137,7 +137,7 @@ Sollen diese aktualisiert werden? %1$s Verbinden mit %1$s - Kompatibilität mit Ihrem Gerät wird überprüft… + Kompatibilität mit Ihrem Gerät wird überprüft … App-Details speichern (%1$d%%) Es werden keine Berechtigungen verwendet. Berechtigungen für Version %s diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 68486ad17..a1c1fdf5c 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -9,7 +9,7 @@ Versión Editar Borrar - Habilitar envío NFC… + Habilitar envío NFC... Caché de aplicaciones descargadas Mantener en la tarjeta SD los ficheros apk descargados No conservar ningún archivo apk @@ -65,7 +65,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Actualizando la lista de aplicaciones… Obteniendo la aplicación de ¡No está habilitado NFC! - Ir a los ajustes de NFC… + Ir a los ajustes de NFC... No se encontró método de envío Bluetooth, ¡elige uno! Elegir el método de envío Bluetooth Dirección del repositorio @@ -80,7 +80,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo ¿Deseas actualizarlos? Actualizar repositorios Gestionar Repositorios - Bluetooth FDroid.apk… + Bluetooth FDroid.apk... Preferencias Acerca de Buscar @@ -127,7 +127,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Novedades Recientemente actualizados Repos de FDroid locales - Descubriendo repos locales de FDroid… + Descubriendo repos locales de FDroid... Descargando %2$s / %3$s (%4$d%%) de %1$s diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index f37301049..1493589a6 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -5,12 +5,25 @@ \'%s\'-rekin bat datorren aplikaziorik ez da aurkitu Bertsio berria zaharraren desberdina den gako batekin sinatuta dago. Bertsio berria instalatzeko, aurretik zaharra desinstalatu beharra dago. Mesedez, egizu eta saiatu berriro. (Kontutan izan desinstalatzean aplikazioak gordetako barne datuak ezabatuko direla) Bertsioa + Editatu + Ezabatu + Gaitu NFC bidez bidaltzea... Gorde cache-an deskargatutako aplikazioak + Mantendu deskargatutako apk fitxategiak SD txartelean + Ez mantendu apk fitxategirik Eguneraketak Biltegiaren azken eskaneatzea: %s inoiz ez + Wifian soilik + Eguneratu aplikazioen zerrendak automatikoki wifian soilik + Beti eguneratu aplikazioen zerrendak automatikoki Jakinarazi + Jakinarazi eguneraketak eskuragarri daudenean + Ez jakinarazi eguneraketak daudenean Eguneratu historia + Bilaketaren emaitzak + Aplikazioaren xehetasunak + Ez da aplikaziorik aurkitu F-Droid-i buruz Jatorrian Aptoide-n oinarritua. GNU GPLv3 lizentziapean argitaratua. @@ -27,6 +40,9 @@ GNU GPLv3 lizentziapean argitaratua. Gehitu biltegi berria Gehitu Utzi + Gaitu + Gehitu gakoa + Gainidatzi Aukeratu biltegia ezabatzeko Eguneratu biltegiak Eskuragarri @@ -37,7 +53,10 @@ GNU GPLv3 lizentziapean argitaratua. Mesedez itxaron Aplikazio-zerrenda eguneratzen… Aplikazioa eskuratzen hemendik + NFC ez dago gaituta! + Joan NFC ezarpenetara... Biltegiaren helbidea + Biltegi hau dagoeneko existitzen da! Erabilitako biltegien zerrenda aldatu egin da. Eguneratu nahi dituzu? Eguneratu biltegiak @@ -47,12 +66,17 @@ Eguneratu nahi dituzu? Bilatu Biltegi berria Ezabatu biltegia + Bilatu biltegi lokalak Exekutatu + Partekatu Instalatu Desinstalatu + Ezikusi eguneraketa guztiak + Ezikusi eguneraketa hau Webgunea Gaiak Iturburu-kodea + Bertsio-berritu Egin dohaintza %s bertsioa instalatuta Instalatu gabe @@ -63,11 +87,13 @@ Eguneratu nahi dituzu? Aditua Bilatu aplikazioak Aplikazioen bateragarritasuna + Bertsio bateraezinak Root Ezikusi egin ukipen-pantailari Guztia Zer da berria Azkenaldian eguneratua + FDroid biltegi lokalak %1$s(e)ra konektatzen Aplikazioak zure gailuarekin bateragarriak diren egiaztatzen… @@ -75,4 +101,14 @@ konektatzen %s bertsioarentzako baimenak Erakutsi baimenak Diseinu trinkoa + Gaia + Sinatu gabea + URLa + Aplikazio kopurua + Deskribapena + Azken eguneraketa + Eguneratu + Izena + Ezezaguna + Biltegia ezabatu? diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 89b82eae6..4c2d22a17 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -64,7 +64,7 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo Aggiornamento elenco applicazioni… Scaricamento applicazione da NFC non attivato! - Vai alle Impostazioni NFC… + Vai alle Impostazioni NFC... Nessun metodo d\'invio via Bluetooth trovato, selezionane uno! Indirizzo repository Impronta digitale (opzionale) From bfdfb6d5efb034ff482cebb7880973061b7861de Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Sun, 23 Feb 2014 09:54:21 +1100 Subject: [PATCH 219/282] Removed unused code from FDroidApp. From before content providers, where we rolled our own update notification system for when data changed in the database. I also removed the property "ctx", because it is availble in getApplicationContext(). As a general rule, it is usually safer to not use a member field if not neccesary. That way, there doesn't need to be any assumptions about when it is set and what value it has. In this case, it was only set half way through onCreate, and therefore usage before then would break. --- src/org/fdroid/fdroid/FDroid.java | 4 -- src/org/fdroid/fdroid/FDroidApp.java | 37 +++---------------- .../fdroid/fdroid/PreferencesActivity.java | 2 - src/org/fdroid/fdroid/data/RepoProvider.java | 2 - 4 files changed, 5 insertions(+), 40 deletions(-) diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index e280610e4..9c55dc2fa 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -281,10 +281,6 @@ public class FDroid extends FragmentActivity { // check if the particular setting has actually been changed. UpdateService.schedule(getBaseContext()); - if ((resultCode & PreferencesActivity.RESULT_RELOAD) != 0) { - ((FDroidApp) getApplication()).invalidateAllApps(); - } - if ((resultCode & PreferencesActivity.RESULT_RESTART) != 0) { ((FDroidApp) getApplication()).reloadTheme(); final Intent intent = getIntent(); diff --git a/src/org/fdroid/fdroid/FDroidApp.java b/src/org/fdroid/fdroid/FDroidApp.java index a5d30acf7..e0d6f2925 100644 --- a/src/org/fdroid/fdroid/FDroidApp.java +++ b/src/org/fdroid/fdroid/FDroidApp.java @@ -129,13 +129,11 @@ public class FDroidApp extends Application { } } - invalidApps = new ArrayList(); - ctx = getApplicationContext(); - UpdateService.schedule(ctx); + UpdateService.schedule(getApplicationContext()); - ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(ctx) + ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(getApplicationContext()) .discCache(new LimitedAgeDiscCache( - new File(StorageUtils.getCacheDirectory(ctx, true), + new File(StorageUtils.getCacheDirectory(getApplicationContext(), true), "icons"), new FileNameGenerator() { @Override @@ -170,8 +168,8 @@ public class FDroidApp extends Application { * compose a chain of trust managers as follows: * MemorizingTrustManager -> Pinning Trust Manager -> System Trust Manager */ - PinningTrustManager pinMgr = new PinningTrustManager(SystemKeyStore.getInstance(ctx),FDroidCertPins.getPinList(), 0); - MemorizingTrustManager memMgr = new MemorizingTrustManager(ctx, pinMgr, defaultTrustManager); + PinningTrustManager pinMgr = new PinningTrustManager(SystemKeyStore.getInstance(getApplicationContext()),FDroidCertPins.getPinList(), 0); + MemorizingTrustManager memMgr = new MemorizingTrustManager(getApplicationContext(), pinMgr, defaultTrustManager); /* * initialize a SSLContext with the outermost trust manager, use this @@ -189,29 +187,4 @@ public class FDroidApp extends Application { } } - private Context ctx; - - // Set when something has changed (database or installed apps) so we know - // we should invalidate the apps. - private Semaphore appsInvalidLock = new Semaphore(1, false); - private List invalidApps; - - // Set apps invalid. Call this when the database has been updated with - // new app information, or when the installed packages have changed. - public void invalidateAllApps() { - try { - appsInvalidLock.acquire(); - } catch (InterruptedException e) { - // Don't care - } finally { - appsInvalidLock.release(); - } - } - - // Invalidate a single app - public void invalidateApp(String id) { - Log.d("FDroid", "Invalidating "+id); - invalidApps.add(id); - } - } diff --git a/src/org/fdroid/fdroid/PreferencesActivity.java b/src/org/fdroid/fdroid/PreferencesActivity.java index 12ac901bd..924cc1f2b 100644 --- a/src/org/fdroid/fdroid/PreferencesActivity.java +++ b/src/org/fdroid/fdroid/PreferencesActivity.java @@ -36,7 +36,6 @@ import org.fdroid.fdroid.compat.ActionBarCompat; public class PreferencesActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener { - public static final int RESULT_RELOAD = 1; public static final int RESULT_RESTART = 4; private int result = 0; @@ -184,7 +183,6 @@ public class PreferencesActivity extends PreferenceActivity implements @Override public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String key) { - updateSummary(key, true); } diff --git a/src/org/fdroid/fdroid/data/RepoProvider.java b/src/org/fdroid/fdroid/data/RepoProvider.java index 5be9fd44b..148ebfb09 100644 --- a/src/org/fdroid/fdroid/data/RepoProvider.java +++ b/src/org/fdroid/fdroid/data/RepoProvider.java @@ -169,8 +169,6 @@ public class RepoProvider extends FDroidProvider { Uri appUri = AppProvider.getNoApksUri(); int appCount = resolver.delete(appUri, null, null); Log.d("Log", "Removed " + appCount + " apps with no apks."); - - app.invalidateAllApps(); } public static int countAppsForRepo(Context context, long repoId) { From 9703350f41485e2e77b7afdb31a3693b4ae243ac Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Sun, 23 Feb 2014 10:05:36 +1100 Subject: [PATCH 220/282] Update tab refreshes correctly now. Before it was listening for changes, but we weren't notifying of changes correctly. Now we use ContentResolver.notifyChange(). --- src/org/fdroid/fdroid/AppDetails.java | 12 ++++++++++-- src/org/fdroid/fdroid/PackageReceiver.java | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 87c552db0..ba1ff18e4 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -909,7 +909,7 @@ public class AppDetails extends ListActivity { Uri uri = Uri.fromParts("package", pkginfo.packageName, null); Intent intent = new Intent(Intent.ACTION_DELETE, uri); startActivityForResult(intent, REQUEST_UNINSTALL); - ((FDroidApp) getApplication()).invalidateApp(id); + notifyAppChanged(id); } @@ -927,7 +927,15 @@ public class AppDetails extends ListActivity { "application/vnd.android.package-archive"); extraNotUnknownSource(intent); startActivityForResult(intent, REQUEST_INSTALL); - ((FDroidApp) getApplication()).invalidateApp(id); + notifyAppChanged(id); + } + + /** + * We could probably drop this, and let the PackageReceiver take care of notifications + * for us, but I don't think the package receiver notifications are very instantaneous. + */ + private void notifyAppChanged(String id) { + getContentResolver().notifyChange(AppProvider.getContentUri(id), null); } private void launchApk(String id) { diff --git a/src/org/fdroid/fdroid/PackageReceiver.java b/src/org/fdroid/fdroid/PackageReceiver.java index c58bf99ff..08aebc755 100644 --- a/src/org/fdroid/fdroid/PackageReceiver.java +++ b/src/org/fdroid/fdroid/PackageReceiver.java @@ -22,6 +22,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; +import org.fdroid.fdroid.data.AppProvider; public class PackageReceiver extends BroadcastReceiver { @@ -29,7 +30,7 @@ public class PackageReceiver extends BroadcastReceiver { public void onReceive(Context ctx, Intent intent) { String appid = intent.getData().getSchemeSpecificPart(); Log.d("FDroid", "PackageReceiver received "+appid); - ((FDroidApp) ctx.getApplicationContext()).invalidateApp(appid); + ctx.getContentResolver().notifyChange(AppProvider.getContentUri(appid), null); Utils.clearInstalledApksCache(); } From 3ebad383d648715b13b690a7462ea39a66dbdc3a Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Sun, 23 Mar 2014 22:31:06 +1100 Subject: [PATCH 221/282] Refactored the recent MR for dynamicly adding default repos. The idea was good: reduce the amount of copied/pasted code where ContentValues were initialized, populated, then inserted. I've kept the idea, by putting it in its own method which is called twice. But the resources are not loaded dynamically any more. This is so that the compiler will be able to pick up if we reference a missing resource. Also, I took the opportunity to replace the field name string literals with references to RepoProvider.DataColumns.* constants. Finally, changed the tests around because now we need to have the "getInteger()" call mocked in resources correctly (for priority/inUse). --- src/org/fdroid/fdroid/data/DBHelper.java | 83 +++++++++---------- test/src/mock/MockCategoryResources.java | 7 +- test/src/mock/MockFDroidResources.java | 36 ++++++++ .../org/fdroid/fdroid/AppProviderTest.java | 8 +- .../org/fdroid/fdroid/FDroidProviderTest.java | 9 ++ 5 files changed, 95 insertions(+), 48 deletions(-) create mode 100644 test/src/mock/MockFDroidResources.java diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index feb323d91..c75fe67e9 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -177,55 +177,46 @@ public class DBHelper extends SQLiteOpenHelper { public void onCreate(SQLiteDatabase db) { createAppApk(db); - db.execSQL(CREATE_TABLE_REPO); - Resources ress = context.getResources(); + insertRepo( + db, + context.getString(R.string.default_repo_name1), + context.getString(R.string.default_repo_address1), + context.getString(R.string.default_repo_description1), + context.getString(R.string.default_repo_pubkey1), + context.getResources().getInteger(R.integer.default_repo_inuse1), + context.getResources().getInteger(R.integer.default_repo_priority1) + ); - int repoCount = ress.getInteger(R.integer.default_repo_count); - for (int i = 1; i <= repoCount; i++) { - ContentValues values = new ContentValues(); - String repoName = context.getString(ress.getIdentifier( - "default_repo_name" + i, - "string", - "org.fdroid.fdroid" - )); - values.put("address", - context.getString(ress.getIdentifier( - "default_repo_address" + i, - "string", - "org.fdroid.fdroid" - ))); - values.put("name",repoName); - values.put("description", - context.getString(ress.getIdentifier( - "default_repo_description" + i, - "string", - "org.fdroid.fdroid" - ))); - String pubkey = context.getString(ress.getIdentifier( - "default_repo_pubkey" + i, - "string", - "org.fdroid.fdroid" - )); - String fingerprint = Utils.calcFingerprint(pubkey); - values.put("pubkey", pubkey); - values.put("fingerprint", fingerprint); - values.put("maxage", 0); - values.put("inuse", ress.getInteger(ress.getIdentifier( - "default_repo_inuse" + i, - "integer", - "org.fdroid.fdroid" - ))); - values.put("priority", ress.getInteger(ress.getIdentifier( - "default_repo_priority" + i, - "integer", - "org.fdroid.fdroid" - ))); - values.put("lastetag", (String) null); - Log.i("FDroid", "Add repository " + repoName); - db.insert(TABLE_REPO, null, values); - } + insertRepo( + db, + context.getString(R.string.default_repo_name2), + context.getString(R.string.default_repo_address2), + context.getString(R.string.default_repo_description2), + context.getString(R.string.default_repo_pubkey2), + context.getResources().getInteger(R.integer.default_repo_inuse2), + context.getResources().getInteger(R.integer.default_repo_priority2) + ); + } + + private void insertRepo( + SQLiteDatabase db, String name, String address, String description, + String pubKey, int inUse, int priority) { + + ContentValues values = new ContentValues(); + values.put(RepoProvider.DataColumns.ADDRESS, address); + values.put(RepoProvider.DataColumns.NAME, name); + values.put(RepoProvider.DataColumns.DESCRIPTION, description); + values.put(RepoProvider.DataColumns.PUBLIC_KEY, pubKey); + values.put(RepoProvider.DataColumns.FINGERPRINT, Utils.calcFingerprint(pubKey)); + values.put(RepoProvider.DataColumns.MAX_AGE, 0); + values.put(RepoProvider.DataColumns.IN_USE, inUse); + values.put(RepoProvider.DataColumns.PRIORITY, priority); + values.put(RepoProvider.DataColumns.LAST_ETAG, (String)null); + + Log.i("FDroid", "Adding repository " + name); + db.insert(TABLE_REPO, null, values); } @Override diff --git a/test/src/mock/MockCategoryResources.java b/test/src/mock/MockCategoryResources.java index 669ddd1d0..2831049d8 100644 --- a/test/src/mock/MockCategoryResources.java +++ b/test/src/mock/MockCategoryResources.java @@ -1,9 +1,14 @@ package mock; +import android.content.Context; import android.test.mock.*; import org.fdroid.fdroid.*; -public class MockCategoryResources extends MockResources { +public class MockCategoryResources extends MockFDroidResources { + + public MockCategoryResources(Context getStringDelegatingContext) { + super(getStringDelegatingContext); + } @Override public String getString(int id) { diff --git a/test/src/mock/MockFDroidResources.java b/test/src/mock/MockFDroidResources.java new file mode 100644 index 000000000..c2b716d2f --- /dev/null +++ b/test/src/mock/MockFDroidResources.java @@ -0,0 +1,36 @@ +package mock; + +import android.content.Context; +import android.content.res.Resources; +import android.test.mock.*; +import org.fdroid.fdroid.*; + +public class MockFDroidResources extends MockResources { + + private Context getStringDelegatingContext; + + public MockFDroidResources(Context getStringDelegatingContext) { + this.getStringDelegatingContext = getStringDelegatingContext; + } + + @Override + public String getString(int id) { + return getStringDelegatingContext.getString(id); + } + + @Override + public int getInteger(int id) { + if (id == R.integer.default_repo_inuse1) { + return 1; + } else if (id == R.integer.default_repo_inuse2) { + return 0; + } else if (id == R.integer.default_repo_priority1) { + return 10; + } else if (id == R.integer.default_repo_priority2) { + return 20; + } else { + return 0; + } +} + +} diff --git a/test/src/org/fdroid/fdroid/AppProviderTest.java b/test/src/org/fdroid/fdroid/AppProviderTest.java index 80e19bd72..e36a32ba3 100644 --- a/test/src/org/fdroid/fdroid/AppProviderTest.java +++ b/test/src/org/fdroid/fdroid/AppProviderTest.java @@ -2,6 +2,7 @@ package org.fdroid.fdroid; import android.content.ContentResolver; import android.content.ContentValues; +import android.content.res.Resources; import android.database.Cursor; import mock.MockCategoryResources; @@ -24,7 +25,12 @@ public class AppProviderTest extends FDroidProviderTest { @Override public void setUp() throws Exception { super.setUp(); - getSwappableContext().setResources(new MockCategoryResources()); + getSwappableContext().setResources(new MockCategoryResources(getContext())); + } + + @Override + protected Resources getMockResources() { + return new MockCategoryResources(getContext()); } @Override diff --git a/test/src/org/fdroid/fdroid/FDroidProviderTest.java b/test/src/org/fdroid/fdroid/FDroidProviderTest.java index 7a4c4a6d6..a2b4a8a18 100644 --- a/test/src/org/fdroid/fdroid/FDroidProviderTest.java +++ b/test/src/org/fdroid/fdroid/FDroidProviderTest.java @@ -3,13 +3,16 @@ package org.fdroid.fdroid; import android.annotation.TargetApi; import android.content.ContentValues; import android.content.Context; +import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.provider.ContactsContract; import android.test.ProviderTestCase2MockContext; +import mock.MockCategoryResources; import mock.MockContextEmptyComponents; import mock.MockContextSwappableComponents; +import mock.MockFDroidResources; import org.fdroid.fdroid.data.FDroidProvider; import org.fdroid.fdroid.mock.MockInstalledApkCache; @@ -23,10 +26,15 @@ public abstract class FDroidProviderTest extends Provi super(providerClass, providerAuthority); } + protected Resources getMockResources() { + return new MockFDroidResources(getContext()); + } + @Override public void setUp() throws Exception { super.setUp(); Utils.setupInstalledApkCache(new MockInstalledApkCache()); + getSwappableContext().setResources(getMockResources()); // The *Provider.Helper.* functions tend to take a Context as their // first parameter. This context is used to connect to the relevant @@ -34,6 +42,7 @@ public abstract class FDroidProviderTest extends Provi // to the mock content resolver, in order to reach the provider // under test. getSwappableContext().setContentResolver(getMockContentResolver()); + } @TargetApi(Build.VERSION_CODES.ECLAIR) From ce3f210919b483d7bfe3e5886d91bc6ae71957f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 23 Mar 2014 12:13:33 +0100 Subject: [PATCH 222/282] Run fix-ellipsis --- res/values-de/strings.xml | 10 +++++----- res/values-es/strings.xml | 8 ++++---- res/values-eu/strings.xml | 4 ++-- res/values-it/strings.xml | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index f324dd74f..0636ae0c8 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -9,7 +9,7 @@ Version Bearbeiten Löschen - NFC-Transfer aktivieren … + NFC-Transfer aktivieren… Anwendungszwischenspeicher Heruntergeladene Programmpakete auf der SD-Karte behalten Installationsdateien nicht behalten @@ -66,7 +66,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Anwendungsliste wird aktualisiert… Anwendung wird heruntergeladen von NFC ist deaktiviert! - NFC-Einstellungen öffnen … + NFC-Einstellungen öffnen… Keine Bluetooth-Sendemethode gefunden. Bitte wählen Sie eine aus! Bluetooth-Sendemethode auswählen Adresse der Paketquelle @@ -81,7 +81,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Sollen diese aktualisiert werden? Paketquellen aktualisieren Paketquellen verwalten - Bluetooth-FDroid.apk … + Bluetooth-FDroid.apk… Einstellungen Über Suchen @@ -128,7 +128,7 @@ Sollen diese aktualisiert werden? Was gibt es Neues Kürzlich Aktualisiert Lokale F-Droid-Paketquellen - Lokale F-Droid-Paketquellen entdecken … + Lokale F-Droid-Paketquellen entdecken… Herunterladen %2$s / %3$s (%4$d%%) von %1$s @@ -137,7 +137,7 @@ Sollen diese aktualisiert werden? %1$s Verbinden mit %1$s - Kompatibilität mit Ihrem Gerät wird überprüft … + Kompatibilität mit Ihrem Gerät wird überprüft… App-Details speichern (%1$d%%) Es werden keine Berechtigungen verwendet. Berechtigungen für Version %s diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index a1c1fdf5c..68486ad17 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -9,7 +9,7 @@ Versión Editar Borrar - Habilitar envío NFC... + Habilitar envío NFC… Caché de aplicaciones descargadas Mantener en la tarjeta SD los ficheros apk descargados No conservar ningún archivo apk @@ -65,7 +65,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Actualizando la lista de aplicaciones… Obteniendo la aplicación de ¡No está habilitado NFC! - Ir a los ajustes de NFC... + Ir a los ajustes de NFC… No se encontró método de envío Bluetooth, ¡elige uno! Elegir el método de envío Bluetooth Dirección del repositorio @@ -80,7 +80,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo ¿Deseas actualizarlos? Actualizar repositorios Gestionar Repositorios - Bluetooth FDroid.apk... + Bluetooth FDroid.apk… Preferencias Acerca de Buscar @@ -127,7 +127,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo Novedades Recientemente actualizados Repos de FDroid locales - Descubriendo repos locales de FDroid... + Descubriendo repos locales de FDroid… Descargando %2$s / %3$s (%4$d%%) de %1$s diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index 1493589a6..88643fd7b 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -7,7 +7,7 @@ Bertsioa Editatu Ezabatu - Gaitu NFC bidez bidaltzea... + Gaitu NFC bidez bidaltzea… Gorde cache-an deskargatutako aplikazioak Mantendu deskargatutako apk fitxategiak SD txartelean Ez mantendu apk fitxategirik @@ -54,7 +54,7 @@ GNU GPLv3 lizentziapean argitaratua. Aplikazio-zerrenda eguneratzen… Aplikazioa eskuratzen hemendik NFC ez dago gaituta! - Joan NFC ezarpenetara... + Joan NFC ezarpenetara… Biltegiaren helbidea Biltegi hau dagoeneko existitzen da! Erabilitako biltegien zerrenda aldatu egin da. diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 4c2d22a17..89b82eae6 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -64,7 +64,7 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo Aggiornamento elenco applicazioni… Scaricamento applicazione da NFC non attivato! - Vai alle Impostazioni NFC... + Vai alle Impostazioni NFC… Nessun metodo d\'invio via Bluetooth trovato, selezionane uno! Indirizzo repository Impronta digitale (opzionale) From 8efa9d609ac9badcf187e43f4dc56a240abcb44f Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 27 Mar 2014 22:22:54 +1100 Subject: [PATCH 223/282] Apps were not getting a current version when upstreamVersioncode not specified. The problem was that they defaulted to 0 if not specified, however the code checking for current version was looking for -1 for a "no upstream version". --- src/org/fdroid/fdroid/UpdateService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index 323ab88fc..c7b46187d 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -405,7 +405,7 @@ public class UpdateService extends IntentService implements ProgressListener { latestcode = apk.vercode; } } - } else if (app.upstreamVercode == -1) { + } else { // If the current version was not set we return the most recent apk. int latestCode = -1; for (Apk apk : apksForApp) { From abdd2fbb8e9c345d557a9b5b6e708a6f72ba1cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 28 Mar 2014 17:59:07 +0100 Subject: [PATCH 224/282] Bring back "Update repos" to the main menu This can later be removed again if the user still has a way to easily update repos manually without having to enter "Manage Repos" and exit again. A good option would be a pull-to-refresh action. --- src/org/fdroid/fdroid/FDroid.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index 9c55dc2fa..6576b8582 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -63,11 +63,12 @@ public class FDroid extends FragmentActivity { public static final String EXTRA_TAB_UPDATE = "extraTab"; - private static final int MANAGE_REPO = Menu.FIRST; - private static final int PREFERENCES = Menu.FIRST + 1; - private static final int ABOUT = Menu.FIRST + 2; - private static final int SEARCH = Menu.FIRST + 3; - private static final int BLUETOOTH_APK = Menu.FIRST + 4; + private static final int UPDATE_REPO = Menu.FIRST; + private static final int MANAGE_REPO = Menu.FIRST + 1; + private static final int PREFERENCES = Menu.FIRST + 2; + private static final int ABOUT = Menu.FIRST + 3; + private static final int SEARCH = Menu.FIRST + 4; + private static final int BLUETOOTH_APK = Menu.FIRST + 5; /* request codes for Bluetooth flows */ private BluetoothAdapter mBluetoothAdapter = null; @@ -143,6 +144,8 @@ public class FDroid extends FragmentActivity { public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); + menu.add(Menu.NONE, UPDATE_REPO, 1, R.string.menu_update_repo).setIcon( + android.R.drawable.ic_menu_rotate); 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( @@ -162,6 +165,10 @@ public class FDroid extends FragmentActivity { switch (item.getItemId()) { + case UPDATE_REPO: + updateRepos(); + return true; + case MANAGE_REPO: Intent i = new Intent(this, ManageRepo.class); startActivityForResult(i, REQUEST_MANAGEREPOS); @@ -260,7 +267,7 @@ public class FDroid extends FragmentActivity { @Override public void onClick(DialogInterface dialog, int whichButton) { - updateRepos(); + updateRepos(); } }); ask_alrt.setNegativeButton(getString(R.string.no), From 05d8e409c4a61e2be829a3e37745688918fd3d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 28 Mar 2014 18:48:08 +0100 Subject: [PATCH 225/282] Remove TODO-before-release --- TODO-before-release.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 TODO-before-release.md diff --git a/TODO-before-release.md b/TODO-before-release.md deleted file mode 100644 index bcca63f4e..000000000 --- a/TODO-before-release.md +++ /dev/null @@ -1,5 +0,0 @@ -These issues are a must-fix before the next stable release: - -* Right after updating a repo, `Recently Updated` shows the apps correctly but - the new apks don't show up on App Details until the whole app is restarted - (or until the repos are wiped and re-downloaded) From 468b6717eee2b17bc2f88d52b321460f46dd2ce0 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 1 Apr 2014 21:19:27 +1100 Subject: [PATCH 226/282] After downloading index, remove apks no longer in the index. It adds an extra 600ms on my Nexus 4 with ~2000 apks from the F-Droid index. But I think it is the only way, as we really need to iterate over every single installed apk, to see if it is still wanted. The up side is that we can query for a large amount of them, rather than quering individually for each apk. NOTE: I haven't added a new status message yet, because we are about to do a stable release. After the stable release, I'll add a new status message to cover for this > half a second (on my relatively fast device). This will probably be part of an overhaul of the update process in general, including a proper progress dialog. --- src/org/fdroid/fdroid/UpdateService.java | 51 +++++++++++++++++---- src/org/fdroid/fdroid/data/ApkProvider.java | 31 ++++++++----- 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index c7b46187d..405793f48 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -313,11 +313,14 @@ public class UpdateService extends IntentService implements ProgressListener { calcIconUrls(this, apksToUpdate, appsToUpdate, repos); calcCurrentApk(apksToUpdate, appsToUpdate); + // Need to do this BEFORE updating the apks, otherwise when it continually + // calls "get apks for repo X" then it will be getting the newly created apks + removeApksNoLongerInRepo(apksToUpdate, updatedRepos); + int totalInsertsUpdates = listOfAppsToUpdate.size() + apksToUpdate.size(); updateOrInsertApps(listOfAppsToUpdate, totalInsertsUpdates, 0); updateOrInsertApks(apksToUpdate, totalInsertsUpdates, listOfAppsToUpdate.size()); removeApksFromRepos(disabledRepos); - removeApksNoLongerInRepo(listOfAppsToUpdate, updatedRepos); removeAppsWithoutApks(); notifyContentProviders(); @@ -618,7 +621,7 @@ public class UpdateService extends IntentService implements ProgressListener { } /** - * Return list of apps from "fromApks" which are already in the database. + * Return list of apps from the "apks" argument which are already in the database. */ private List getKnownApks(List apks) { List knownApks = new ArrayList(); @@ -701,26 +704,54 @@ public class UpdateService extends IntentService implements ProgressListener { * belong to the repo which are not in the current list of apks that were * retrieved. */ - private void removeApksNoLongerInRepo(List appsToUpdate, - List updatedRepos) { + private void removeApksNoLongerInRepo(List apksToUpdate, List updatedRepos) { + + long startTime = System.currentTimeMillis(); + List toRemove = new ArrayList(); + + String[] fields = { + ApkProvider.DataColumns.APK_ID, + ApkProvider.DataColumns.VERSION_CODE, + ApkProvider.DataColumns.VERSION, + }; + for (Repo repo : updatedRepos) { - Log.d("FDroid", "Removing apks no longer in repo " + repo.address); - // TODO: Implement + List existingApks = ApkProvider.Helper.findByRepo(this, repo, fields); + for (Apk existingApk : existingApks) { + if (!isApkToBeUpdated(existingApk, apksToUpdate)) { + toRemove.add(existingApk); + } + } } + long duration = System.currentTimeMillis() - startTime; + Log.d("FDroid", "Found " + toRemove.size() + " apks no longer in the updated repos (took " + duration + "ms)"); + + if (toRemove.size() > 0) { + ApkProvider.Helper.deleteApks(this, toRemove); + } + } + + private static boolean isApkToBeUpdated(Apk existingApk, List apksToUpdate) { + for (Apk apkToUpdate : apksToUpdate) { + if (apkToUpdate.vercode == existingApk.vercode && apkToUpdate.id.equals(existingApk.id)) { + return true; + } + } + return false; } private void removeApksFromRepos(List repos) { for (Repo repo : repos) { - Log.d("FDroid", "Removing apks from repo " + repo.address); Uri uri = ApkProvider.getRepoUri(repo.getId()); - getContentResolver().delete(uri, null, null); + int numDeleted = getContentResolver().delete(uri, null, null); + Log.d("FDroid", "Removing " + numDeleted + " apks from repo " + repo.address); } } private void removeAppsWithoutApks() { - Log.d("FDroid", "Removing aps that don't have any apks"); - getContentResolver().delete(AppProvider.getNoApksUri(), null, null); + int numDeleted = getContentResolver().delete(AppProvider.getNoApksUri(), null, null); + Log.d("FDroid", "Removing " + numDeleted + " apks that don't have any apks"); } diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java index 9ecff515a..942af616b 100644 --- a/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -8,6 +8,7 @@ import android.database.Cursor; import android.net.Uri; import android.provider.BaseColumns; import android.util.Log; +import org.fdroid.fdroid.UpdateService; import java.util.*; @@ -59,6 +60,12 @@ public class ApkProvider extends FDroidProvider { resolver.delete(uri, null, null); } + public static void deleteApks(Context context, List apks) { + ContentResolver resolver = context.getContentResolver(); + Uri uri = getContentUri(apks); + resolver.delete(uri, null, null); + } + public static Apk find(Context context, String id, int versionCode) { return find(context, id, versionCode, DataColumns.ALL); } @@ -102,6 +109,13 @@ public class ApkProvider extends FDroidProvider { Cursor cursor = resolver.query(uri, fields, null, null, null); return cursorToList(cursor); } + + public static List findByRepo(Context context, Repo repo, String[] fields) { + ContentResolver resolver = context.getContentResolver(); + Uri uri = getRepoUri(repo.getId()); + Cursor cursor = resolver.query(uri, fields, null, null, null); + return cursorToList(cursor); + } } public interface DataColumns extends BaseColumns { @@ -392,20 +406,15 @@ public class ApkProvider extends FDroidProvider { query = query.add(queryApp(uri.getLastPathSegment())); break; - case CODE_LIST: - throw new UnsupportedOperationException( - "Can't delete all apks. " + - "Can only delete those belonging to an app, or a repo."); - case CODE_APKS: - throw new UnsupportedOperationException( - "Can't delete arbitrary apks. " + - "Can only delete those belonging to an app, or a repo."); + query = query.add(queryApks(uri.getLastPathSegment())); + break; + + case CODE_LIST: + throw new UnsupportedOperationException("Can't delete all apks."); case CODE_SINGLE: - throw new UnsupportedOperationException( - "Can't delete individual apks. " + - "Can only delete those belonging to an app, or a repo."); + throw new UnsupportedOperationException("Can't delete individual apks."); default: Log.e("FDroid", "Invalid URI for apk content provider: " + uri); From ce334e215dff11b0198231f7300ba06b79985079 Mon Sep 17 00:00:00 2001 From: Ciaran Gultnieks Date: Tue, 1 Apr 2014 11:57:10 +0100 Subject: [PATCH 227/282] Add Hungarian translations by gidano --- res/values-hu/arrays.xml | 19 +++++ res/values-hu/strings.xml | 155 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 res/values-hu/arrays.xml create mode 100644 res/values-hu/strings.xml diff --git a/res/values-hu/arrays.xml b/res/values-hu/arrays.xml new file mode 100644 index 000000000..59a86865b --- /dev/null +++ b/res/values-hu/arrays.xml @@ -0,0 +1,19 @@ + + + + Soha + Óránként + 4 óránként + 12 óránként + Naponta + + + Sötét + Világos + + + Ki (nem biztonságos) + Normál + Teljes + + diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml new file mode 100644 index 000000000..331bc1023 --- /dev/null +++ b/res/values-hu/strings.xml @@ -0,0 +1,155 @@ + + + F-Droid + F-Droid Archive + https://f-droid.org/repo + https://f-droid.org/archive + Hivatalos FDroid repository. A repoban található alkalmazások javarészt forráskódból épített appok. Néhány binárist hivatásos alkalmazás fejlesztők építettek - ezeket idővel fel fogják váltani a forráskódból építettek. + Az F-Droid kliens archiv repoja Ez tartalmazza a fő tároló régebbi verziójú alkalmazásait. + 3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef + F-Droid + 0.58 + https://f-droid.org + team@f-droid.org + Bitcoin + Litecoin + Dogecoin + Flattr + "%1$d alkalmazás találat ehhez '%2$s':" + "Egy alkalmazás találat ehhez '%s':" + "Nincs ezzel '%s' egyező alkalmazás" + Az új verzió a régitől eltérő kulccsal van aláírva. Az új verzió telepítéséhez először el kell távolítani a régit. Kérem tegye meg ezt és próbálja újra. (Megjegyzendő, hogy az eltávolítás törli az alkalmazás minden tárolt adatát) + Úgy tűnik, ez a csomag nem kompatibilis a készülékkel. Ennek ellenére is megpróbálja telepíteni? + Ön megpróbálta régebbivel felülírni az alkalmazást. Ez hibás működést és az adatok elvesztését okozhatja. Ennek ellenére is megpróbálja telepíteni? + Verzió + App cache + A letöltött apk fájlt megtartja az SD kártyán + Ne tartson meg semmilyen apk fájlt + Frissítések + egyéb + Utolsó vizsgálat: %s + soha + Auto. frissítés intervalluma + Ne frissítsen automatikusan + Csak Wifin + Automatikus app lista frissítés csak Wifin + Mindig frissítse az app listákat automatikusan + Értesítés + Értesít ha frissítés érhető el + Ne jelezzen semmilyen frissítést + Frissítési előzmények + Hány napig tekintse az appot frissnek vagy újnak: %s + Keresési előzmények + App részletei + Nem található ilyen app + Az F-Droid + "Eredetileg az Aptoide alapján. +Kiadva GNU GPLv3 licensz alatt.\n\nHonosítás: gidano | sympda.info" + Weboldal: + Email: + Verzió: + Weboldal + "Még nincs konfigurálva repository! + +A repo, alkalmazások forrása. Hozzáadáshoz nyomja meg a MENÜ gombot és írja be a címét. + +A repo cím valahogy így néz ki: https://f-droid.org/repo" + Telepítve + Nincs telepítve + Hozzáadva %s + Oké + Igen + Nem + Új repo hozzáadása + Hozzáad + Mégsem + Engedélyez + Kulcs hozzáadás + Felülírás + Válasszon ki repo-t az eltávolításhoz + Repo-k frissítése + Elérhető + Frissítések + 1 frissítés érhető el. + %d frissítés érhető el. + F-Droid frissítés érhető el + Kérem várjon + Alkalmazáslista frissítése... + Alkalmazás beszerzése + Repo cím + ujjlenyomat (opcionális) + Ez a repo már létezik! + Ez a repo már be van állítva, ez új kulcs információt fog hozzáadni. + Ez a repo már be van állítva, erősítse meg az újraengedélyezését. + A fogadott repo már telepítve és engedélyezve van! + Előbb törölnie kell ezt a repot, mielőtt hozzáad egyet eltérő kulccsal! + "A használt repo-k listája megváltozott. +Szeretné ezeket frissíteni?" + Repo-k frissítése + Repo-k kezelése + Lehetőségek + Rólunk + Keresés + Új repository + Repo eltávolítása + Indítás + Megoszt + Telepít + Eltávolít + Mellőz minden frissítést + Ezt a frissítést mellőzi + Weboldal + Kérdések + Forráskód + Frissítés + Adomány + %s verzió telepítve + Nincs telepítve + A letöltött fájl hibás + Letöltés megszakítva + Az alkalmazás reklámot tartalmaz + Az alkalmazás nyomon követi és jelenti a tevékenységeit + Az alkalmazás nem ingyenes kiegészítőket támogat + Az alkalmazás nem ingyenes hálózati szolgáltatásokat támogat + Az alkalmazás egyéb, nem ingyenes alkalmazásoktól függ + Az upstream forráskód nem teljesen szabad + Megjelenés + Szakértő + Extra infókat mutat és extra beállításokat engedélyez + Extrák elrejtése gyakorlott felhasználók számára + Alkalmazások keresése + Adatbázis szink. módja + App kompatibilitás + Inkompatibilis verzió + Megjeleníti, ha az app verzió nem kompatibilis a készülékkel + Elrejti az app inkompatibilitását az eszközzel + Root + Nem szürkíti ki a root jogot igénylő alkalmazásokat + Kiszürkíti a root jogot igénylő alkalmazásokat + Érintőképernyő ignorálás + Mindig tartalmazza az érintőképernyőt igénylő alkalmazásokat + Appok normál szűrése + Összes + "Újdonságok" + Legutóbb frissítve + "Letöltés +%2$s / %3$s (%4$d%%) innen +%1$s" + "Feldolgozási kérelem +%2$d of %3$d innen +%1$s" + "Kapcsolódás +%1$s" + Alkalmazások eszközkompatibilitási ellenőrzése… + Nincsenek engedélyei. + Engedélyek erre a verzióra - %s + Engedélyek megjelenítése + Mutassa meg egy listában az app által igényelt engedélyeket + "Ne mutasson listát letöltés előtt" + "Nincs semmilyen app, ami képes ezt kezelni %s" + Kompakt elrendezés + Ikonok kisebb méretben + Ikonok normál méretben + Téma + Android %s vagy későbbi + From 4f0cba0c84f06232fa76813495abd0f0b7575497 Mon Sep 17 00:00:00 2001 From: F-Droid Translatebot Date: Tue, 1 Apr 2014 12:16:44 +0100 Subject: [PATCH 228/282] Translation updates --- res/values-hu/strings.xml | 296 ++++++++++++++++++-------------------- 1 file changed, 141 insertions(+), 155 deletions(-) diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml index 331bc1023..bbdde388e 100644 --- a/res/values-hu/strings.xml +++ b/res/values-hu/strings.xml @@ -1,155 +1,141 @@ - - - F-Droid - F-Droid Archive - https://f-droid.org/repo - https://f-droid.org/archive - Hivatalos FDroid repository. A repoban található alkalmazások javarészt forráskódból épített appok. Néhány binárist hivatásos alkalmazás fejlesztők építettek - ezeket idővel fel fogják váltani a forráskódból építettek. - Az F-Droid kliens archiv repoja Ez tartalmazza a fő tároló régebbi verziójú alkalmazásait. - 3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef - F-Droid - 0.58 - https://f-droid.org - team@f-droid.org - Bitcoin - Litecoin - Dogecoin - Flattr - "%1$d alkalmazás találat ehhez '%2$s':" - "Egy alkalmazás találat ehhez '%s':" - "Nincs ezzel '%s' egyező alkalmazás" - Az új verzió a régitől eltérő kulccsal van aláírva. Az új verzió telepítéséhez először el kell távolítani a régit. Kérem tegye meg ezt és próbálja újra. (Megjegyzendő, hogy az eltávolítás törli az alkalmazás minden tárolt adatát) - Úgy tűnik, ez a csomag nem kompatibilis a készülékkel. Ennek ellenére is megpróbálja telepíteni? - Ön megpróbálta régebbivel felülírni az alkalmazást. Ez hibás működést és az adatok elvesztését okozhatja. Ennek ellenére is megpróbálja telepíteni? - Verzió - App cache - A letöltött apk fájlt megtartja az SD kártyán - Ne tartson meg semmilyen apk fájlt - Frissítések - egyéb - Utolsó vizsgálat: %s - soha - Auto. frissítés intervalluma - Ne frissítsen automatikusan - Csak Wifin - Automatikus app lista frissítés csak Wifin - Mindig frissítse az app listákat automatikusan - Értesítés - Értesít ha frissítés érhető el - Ne jelezzen semmilyen frissítést - Frissítési előzmények - Hány napig tekintse az appot frissnek vagy újnak: %s - Keresési előzmények - App részletei - Nem található ilyen app - Az F-Droid - "Eredetileg az Aptoide alapján. -Kiadva GNU GPLv3 licensz alatt.\n\nHonosítás: gidano | sympda.info" - Weboldal: - Email: - Verzió: - Weboldal - "Még nincs konfigurálva repository! - -A repo, alkalmazások forrása. Hozzáadáshoz nyomja meg a MENÜ gombot és írja be a címét. - -A repo cím valahogy így néz ki: https://f-droid.org/repo" - Telepítve - Nincs telepítve - Hozzáadva %s - Oké - Igen - Nem - Új repo hozzáadása - Hozzáad - Mégsem - Engedélyez - Kulcs hozzáadás - Felülírás - Válasszon ki repo-t az eltávolításhoz - Repo-k frissítése - Elérhető - Frissítések - 1 frissítés érhető el. - %d frissítés érhető el. - F-Droid frissítés érhető el - Kérem várjon - Alkalmazáslista frissítése... - Alkalmazás beszerzése - Repo cím - ujjlenyomat (opcionális) - Ez a repo már létezik! - Ez a repo már be van állítva, ez új kulcs információt fog hozzáadni. - Ez a repo már be van állítva, erősítse meg az újraengedélyezését. - A fogadott repo már telepítve és engedélyezve van! - Előbb törölnie kell ezt a repot, mielőtt hozzáad egyet eltérő kulccsal! - "A használt repo-k listája megváltozott. -Szeretné ezeket frissíteni?" - Repo-k frissítése - Repo-k kezelése - Lehetőségek - Rólunk - Keresés - Új repository - Repo eltávolítása - Indítás - Megoszt - Telepít - Eltávolít - Mellőz minden frissítést - Ezt a frissítést mellőzi - Weboldal - Kérdések - Forráskód - Frissítés - Adomány - %s verzió telepítve - Nincs telepítve - A letöltött fájl hibás - Letöltés megszakítva - Az alkalmazás reklámot tartalmaz - Az alkalmazás nyomon követi és jelenti a tevékenységeit - Az alkalmazás nem ingyenes kiegészítőket támogat - Az alkalmazás nem ingyenes hálózati szolgáltatásokat támogat - Az alkalmazás egyéb, nem ingyenes alkalmazásoktól függ - Az upstream forráskód nem teljesen szabad - Megjelenés - Szakértő - Extra infókat mutat és extra beállításokat engedélyez - Extrák elrejtése gyakorlott felhasználók számára - Alkalmazások keresése - Adatbázis szink. módja - App kompatibilitás - Inkompatibilis verzió - Megjeleníti, ha az app verzió nem kompatibilis a készülékkel - Elrejti az app inkompatibilitását az eszközzel - Root - Nem szürkíti ki a root jogot igénylő alkalmazásokat - Kiszürkíti a root jogot igénylő alkalmazásokat - Érintőképernyő ignorálás - Mindig tartalmazza az érintőképernyőt igénylő alkalmazásokat - Appok normál szűrése - Összes - "Újdonságok" - Legutóbb frissítve - "Letöltés -%2$s / %3$s (%4$d%%) innen -%1$s" - "Feldolgozási kérelem -%2$d of %3$d innen -%1$s" - "Kapcsolódás -%1$s" - Alkalmazások eszközkompatibilitási ellenőrzése… - Nincsenek engedélyei. - Engedélyek erre a verzióra - %s - Engedélyek megjelenítése - Mutassa meg egy listában az app által igényelt engedélyeket - "Ne mutasson listát letöltés előtt" - "Nincs semmilyen app, ami képes ezt kezelni %s" - Kompakt elrendezés - Ikonok kisebb méretben - Ikonok normál méretben - Téma - Android %s vagy későbbi - + + + \"%1$d alkalmazás találat ehhez \'%2$s\':\" + \"Egy alkalmazás találat ehhez \'%s\':\" + \"Nincs ezzel \'%s\' egyező alkalmazás\" + Az új verzió a régitől eltérő kulccsal van aláírva. Az új verzió telepítéséhez először el kell távolítani a régit. Kérem tegye meg ezt és próbálja újra. (Megjegyzendő, hogy az eltávolítás törli az alkalmazás minden tárolt adatát) + Úgy tűnik, ez a csomag nem kompatibilis a készülékkel. Ennek ellenére is megpróbálja telepíteni? + Ön megpróbálta régebbivel felülírni az alkalmazást. Ez hibás működést és az adatok elvesztését okozhatja. Ennek ellenére is megpróbálja telepíteni? + Verzió + App cache + A letöltött apk fájlt megtartja az SD kártyán + Ne tartson meg semmilyen apk fájlt + Frissítések + egyéb + Utolsó vizsgálat: %s + soha + Auto. frissítés intervalluma + Ne frissítsen automatikusan + Csak Wifin + Automatikus app lista frissítés csak Wifin + Mindig frissítse az app listákat automatikusan + Értesítés + Értesít ha frissítés érhető el + Ne jelezzen semmilyen frissítést + Frissítési előzmények + Hány napig tekintse az appot frissnek vagy újnak: %s + Keresési előzmények + App részletei + Nem található ilyen app + Az F-Droid + \"Eredetileg az Aptoide alapján. +Kiadva GNU GPLv3 licensz alatt. + +Honosítás: gidano | sympda.info\" + Weboldal: + Email: + Verzió: + Weboldal + \"Még nincs konfigurálva repository! + +A repo, alkalmazások forrása. Hozzáadáshoz nyomja meg a MENÜ gombot és írja be a címét. + +A repo cím valahogy így néz ki: https://f-droid.org/repo\" + Telepítve + Nincs telepítve + Hozzáadva %s + Oké + Igen + Nem + Új repo hozzáadása + Hozzáad + Mégsem + Engedélyez + Kulcs hozzáadás + Felülírás + Válasszon ki repo-t az eltávolításhoz + Repo-k frissítése + Elérhető + Frissítések + 1 frissítés érhető el. + %d frissítés érhető el. + F-Droid frissítés érhető el + Kérem várjon + Alkalmazáslista frissítése... + Alkalmazás beszerzése + Repo cím + ujjlenyomat (opcionális) + Ez a repo már létezik! + Ez a repo már be van állítva, ez új kulcs információt fog hozzáadni. + Ez a repo már be van állítva, erősítse meg az újraengedélyezését. + A fogadott repo már telepítve és engedélyezve van! + Előbb törölnie kell ezt a repot, mielőtt hozzáad egyet eltérő kulccsal! + \"A használt repo-k listája megváltozott. +Szeretné ezeket frissíteni?\" + Repo-k frissítése + Repo-k kezelése + Lehetőségek + Rólunk + Keresés + Új repository + Repo eltávolítása + Indítás + Megoszt + Telepít + Eltávolít + Mellőz minden frissítést + Ezt a frissítést mellőzi + Weboldal + Kérdések + Forráskód + Frissítés + Adomány + %s verzió telepítve + Nincs telepítve + A letöltött fájl hibás + Letöltés megszakítva + Az alkalmazás reklámot tartalmaz + Az alkalmazás nyomon követi és jelenti a tevékenységeit + Az alkalmazás nem ingyenes kiegészítőket támogat + Az alkalmazás nem ingyenes hálózati szolgáltatásokat támogat + Az alkalmazás egyéb, nem ingyenes alkalmazásoktól függ + Az upstream forráskód nem teljesen szabad + Megjelenés + Szakértő + Extra infókat mutat és extra beállításokat engedélyez + Extrák elrejtése gyakorlott felhasználók számára + Alkalmazások keresése + App kompatibilitás + Inkompatibilis verzió + Megjeleníti, ha az app verzió nem kompatibilis a készülékkel + Elrejti az app inkompatibilitását az eszközzel + Root + Nem szürkíti ki a root jogot igénylő alkalmazásokat + Kiszürkíti a root jogot igénylő alkalmazásokat + Érintőképernyő ignorálás + Mindig tartalmazza az érintőképernyőt igénylő alkalmazásokat + Appok normál szűrése + Összes + \"Újdonságok\" + Legutóbb frissítve + \"Letöltés +%2$s / %3$s (%4$d%%) innen +%1$s\" + \"Feldolgozási kérelem +%2$d of %3$d innen +%1$s\" + \"Kapcsolódás +%1$s\" + Alkalmazások eszközkompatibilitási ellenőrzése… + Nincsenek engedélyei. + Engedélyek erre a verzióra - %s + Engedélyek megjelenítése + Mutassa meg egy listában az app által igényelt engedélyeket + \"Ne mutasson listát letöltés előtt\" + \"Nincs semmilyen app, ami képes ezt kezelni %s\" + Kompakt elrendezés + Ikonok kisebb méretben + Ikonok normál méretben + Téma + Android %s vagy későbbi + From 6b08ab0e97a703e29cb7ae2eeffd2b3a442a2159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 1 Apr 2014 14:02:26 +0200 Subject: [PATCH 229/282] Place -hu arrays in the correct file --- res/values-hu/{arrays.xml => array.xml} | 38 ++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) rename res/values-hu/{arrays.xml => array.xml} (96%) diff --git a/res/values-hu/arrays.xml b/res/values-hu/array.xml similarity index 96% rename from res/values-hu/arrays.xml rename to res/values-hu/array.xml index 59a86865b..49ef13b1f 100644 --- a/res/values-hu/arrays.xml +++ b/res/values-hu/array.xml @@ -1,19 +1,19 @@ - - - - Soha - Óránként - 4 óránként - 12 óránként - Naponta - - - Sötét - Világos - - - Ki (nem biztonságos) - Normál - Teljes - - + + + + Soha + Óránként + 4 óránként + 12 óránként + Naponta + + + Sötét + Világos + + + Ki (nem biztonságos) + Normál + Teljes + + From 2ff0ae955017a581c000e871ebd90444369bb773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 1 Apr 2014 14:04:32 +0200 Subject: [PATCH 230/282] Run translation scripts --- res/values-hu/array.xml | 5 ----- res/values-hu/strings.xml | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/res/values-hu/array.xml b/res/values-hu/array.xml index 49ef13b1f..a27da5898 100644 --- a/res/values-hu/array.xml +++ b/res/values-hu/array.xml @@ -11,9 +11,4 @@ Sötét Világos - - Ki (nem biztonságos) - Normál - Teljes - diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml index bbdde388e..0802c7f48 100644 --- a/res/values-hu/strings.xml +++ b/res/values-hu/strings.xml @@ -61,7 +61,7 @@ A repo cím valahogy így néz ki: https://f-droid.org/repo\" %d frissítés érhető el. F-Droid frissítés érhető el Kérem várjon - Alkalmazáslista frissítése... + Alkalmazáslista frissítése… Alkalmazás beszerzése Repo cím ujjlenyomat (opcionális) From 44312bdb6c2d69ed4b98ee5ae92462e7969501e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 1 Apr 2014 14:39:27 +0200 Subject: [PATCH 231/282] Bump version to 0.62 --- AndroidManifest.xml | 4 ++-- res/values/no_trans.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 3cd519cc4..4829a4fb6 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="620" + android:versionName="0.62" > F-Droid - 0.61-test + 0.62 https://f-droid.org team@f-droid.org From 232145eac17af50735d47840869972629c1782bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 1 Apr 2014 15:08:51 +0200 Subject: [PATCH 232/282] Update repo snippet to latest version --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee5669007..e73703c69 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,11 @@ Add the following lines to your repo manifest: - + - + + + ``` Adding F-Droid is then just a matter of adding `F-Droid` to your `PRODUCT_PACKAGES`. From edd2de49c317ecd59ab7c9c193c7a3b7cc6edc3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 1 Apr 2014 15:16:24 +0200 Subject: [PATCH 233/282] Use HEAD instead of LAST_STABLE_TAG --- tools/repo-revisions.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/repo-revisions.sh b/tools/repo-revisions.sh index 33b2d02dc..911e3a3c3 100755 --- a/tools/repo-revisions.sh +++ b/tools/repo-revisions.sh @@ -2,9 +2,9 @@ # Update README repo manifest revisions from git -LAST_STABLE_TAG=$(git describe --abbrev=0 --tags --match='*[0-9]') -sed -i 's@\(.*name="fdroidclient\.git".*revision="\)[^"]*\(".*\)@\1'$LAST_STABLE_TAG'\2@' README.md +#LAST_STABLE_TAG=$(git describe --abbrev=0 --tags --match='[0-9]*[0-9]') +#sed -i 's@\(.*name="fdroidclient\.git".*revision="\)[^"]*\(".*\)@\1'$LAST_STABLE_TAG'\2@' README.md -git ls-tree $LAST_STABLE_TAG extern/ | while read _ _ revision path; do +git ls-tree HEAD extern/ | while read _ _ revision path; do sed -i 's@\(.*fdroidclient/'$path'".*revision="\)[^"]*\(".*\)@\1'$revision'\2@' README.md done From f0a40d8b6e1f165b75444353c8aac60c39c8ac68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 1 Apr 2014 15:19:14 +0200 Subject: [PATCH 234/282] Use a commit hash instead of 0.62 to test if repo works --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e73703c69..06f056b6c 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Add the following lines to your repo manifest: - + From f6cf7c9b0c7e08559031644e6d4a83d42db1602b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 1 Apr 2014 16:06:01 +0200 Subject: [PATCH 235/282] Revert "Use a commit hash instead of 0.62 to test if repo works" This reverts commit f0a40d8b6e1f165b75444353c8aac60c39c8ac68. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06f056b6c..e73703c69 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Add the following lines to your repo manifest: - + From 8977ac682633361cdac29f3faa5a0cf1341eb4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 1 Apr 2014 16:09:16 +0200 Subject: [PATCH 236/282] Prepare the changelog --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b7d0c7b..4fad30a50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ -### Upcoming release +### 0.62 (2014-04-01) * Support for Network Service Discovery of local FDroid repos on Android 4.1+ - from the repository management screen. + from the repository management screen * Always remember the selected category in the list of apps @@ -22,6 +22,8 @@ * Various fixes to layout issues introduced in 0.58 +* Translation updates + ### 0.58 (2014-01-11) * Download icons with a resolution that matches the device's screen density, @@ -70,6 +72,7 @@ ### 0.55 (2013-11-11) * Fixed problems with category selection and permission lists on Android 2.X devices. + * Lots of translation updates, including new Norwegian translation. ### 0.54 (2013-11-05) From e7eb3120cfa8b05b606f16cf6f833569737fc36b Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 1 Apr 2014 21:58:24 +1100 Subject: [PATCH 237/282] Updated changelogs with (what is hopefully) a lay description of the ContentProvider changes. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fad30a50..63ae5c591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ * Filter app compatibility by maxSdkVersion too +* Major internal changes to enable F-Droid to handle repos with thousands + of apps without slowing down too much. These internal changes will also make + new features easier to implement. + * Various fixes to layout issues introduced in 0.58 * Translation updates From eded748ab82f14cc09c84f48aba6473b2a8ac9ad Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 3 Apr 2014 00:32:56 +1100 Subject: [PATCH 238/282] Fixed the suggestedVersion calculation, now done via SQL. The archive repo was getting updated after the regular repo. In these situations, we didn't have every single app/apk in memory in order to calculate the suggested version. As a result, F-Droid ended up choosing a suggested version from the archived versions, when terhere was actually a newer version in the database. This change does all of the calculations in two database queries now. Although the implementation of the query is not hackey, they way I get to the code in order to execute the query is a bit hacky, so most of the implementation is private. --- src/org/fdroid/fdroid/UpdateService.java | 53 +------- src/org/fdroid/fdroid/data/AppProvider.java | 133 +++++++++++++++++++- 2 files changed, 133 insertions(+), 53 deletions(-) diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index 405793f48..7231b99a6 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -311,7 +311,6 @@ public class UpdateService extends IntentService implements ProgressListener { calcCompatibilityFlags(this, apksToUpdate, appsToUpdate); calcIconUrls(this, apksToUpdate, appsToUpdate, repos); - calcCurrentApk(apksToUpdate, appsToUpdate); // Need to do this BEFORE updating the apks, otherwise when it continually // calls "get apks for repo X" then it will be getting the newly created apks @@ -322,6 +321,7 @@ public class UpdateService extends IntentService implements ProgressListener { updateOrInsertApks(apksToUpdate, totalInsertsUpdates, listOfAppsToUpdate.size()); removeApksFromRepos(disabledRepos); removeAppsWithoutApks(); + AppProvider.Helper.calcSuggestedVersionsForAll(this); notifyContentProviders(); if (prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, false)) { @@ -374,57 +374,6 @@ public class UpdateService extends IntentService implements ProgressListener { } } - /** - * Get the current version - this will be one of the Apks from 'apks'. - * Can return null if there are no available versions. - * This should be the 'current' version, as in the most recent stable - * one, that most users would want by default. It might not be the - * most recent, if for example there are betas etc. - */ - private static void calcCurrentApk(List apks, Map apps ) { - for ( App app : apps.values() ) { - List apksForApp = new ArrayList(); - for (Apk apk : apks) { - if (apk.id.equals(app.id)) { - apksForApp.add(apk); - } - } - calcCurrentApkForApp(app, apksForApp); - } - } - - private static void calcCurrentApkForApp(App app, List apksForApp) { - Apk latestApk = null; - // Try and return the real current version first. It will find the - // closest version smaller than the upstreamVercode, being the same - // vercode if it exists. - if (app.upstreamVercode > 0) { - int latestcode = -1; - for (Apk apk : apksForApp) { - if ((!app.compatible || apk.compatible) - && apk.vercode <= app.upstreamVercode - && apk.vercode > latestcode) { - latestApk = apk; - latestcode = apk.vercode; - } - } - } else { - // If the current version was not set we return the most recent apk. - int latestCode = -1; - for (Apk apk : apksForApp) { - if ((!app.compatible || apk.compatible) - && apk.vercode > latestCode) { - latestApk = apk; - latestCode = apk.vercode; - } - } - } - - if (latestApk != null) { - app.suggestedVercode = latestApk.vercode; - } - } - private static void calcIconUrls(Context context, List apks, Map apps, List repos) { String iconsDir = Utils.getIconsDir(context); diff --git a/src/org/fdroid/fdroid/data/AppProvider.java b/src/org/fdroid/fdroid/data/AppProvider.java index 95176fcdb..a76046bea 100644 --- a/src/org/fdroid/fdroid/data/AppProvider.java +++ b/src/org/fdroid/fdroid/data/AppProvider.java @@ -120,8 +120,19 @@ public class AppProvider extends FDroidProvider { return app; } - public static void deleteAppsWithNoApks(ContentResolver resolver) { + /* + * I wasn't quite sure on the best way to execute arbitrary queries using the same DBHelper as the + * content provider class, so I've hidden the implementation of this (by making it private) in case + * I find a better way in the future. + */ + public static void calcSuggestedVersionsForAll(Context context) { + Uri fromUpstream = calcSuggestedVersionFromUpstream(); + context.getContentResolver().update(fromUpstream, null, null, null); + + Uri fromLatest = calcSuggestedVersionFromLatest(); + context.getContentResolver().update(fromLatest, null, null, null); } + } public interface DataColumns { @@ -235,6 +246,8 @@ public class AppProvider extends FDroidProvider { private static final String PATH_NEWLY_ADDED = "newlyAdded"; private static final String PATH_CATEGORY = "category"; private static final String PATH_IGNORED = "ignored"; + private static final String PATH_CALC_SUGGESTED_FROM_UPSTREAM = "calcSuggestedFromUpstream"; + private static final String PATH_CALC_SUGGESTED_FROM_LATEST = "calcSuggestedFromLatest"; private static final int CAN_UPDATE = CODE_SINGLE + 1; private static final int INSTALLED = CAN_UPDATE + 1; @@ -245,9 +258,13 @@ public class AppProvider extends FDroidProvider { private static final int NEWLY_ADDED = RECENTLY_UPDATED + 1; private static final int CATEGORY = NEWLY_ADDED + 1; private static final int IGNORED = CATEGORY + 1; + private static final int CALC_SUGGESTED_FROM_UPSTREAM = IGNORED + 1; + private static final int CALC_SUGGESTED_FROM_LATEST = CALC_SUGGESTED_FROM_UPSTREAM + 1; static { matcher.addURI(getAuthority(), null, CODE_LIST); + matcher.addURI(getAuthority(), PATH_CALC_SUGGESTED_FROM_UPSTREAM, CALC_SUGGESTED_FROM_UPSTREAM); + matcher.addURI(getAuthority(), PATH_CALC_SUGGESTED_FROM_LATEST, CALC_SUGGESTED_FROM_LATEST); matcher.addURI(getAuthority(), PATH_IGNORED, IGNORED); matcher.addURI(getAuthority(), PATH_RECENTLY_UPDATED, RECENTLY_UPDATED); matcher.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED); @@ -276,6 +293,14 @@ public class AppProvider extends FDroidProvider { return Uri.withAppendedPath(getContentUri(), PATH_IGNORED); } + private static Uri calcSuggestedVersionFromUpstream() { + return Uri.withAppendedPath(getContentUri(), PATH_CALC_SUGGESTED_FROM_UPSTREAM); + } + + private static Uri calcSuggestedVersionFromLatest() { + return Uri.withAppendedPath(getContentUri(), PATH_CALC_SUGGESTED_FROM_LATEST); + } + public static Uri getCategoryUri(String category) { return getContentUri().buildUpon() .appendPath(PATH_CATEGORY) @@ -550,6 +575,14 @@ public class AppProvider extends FDroidProvider { QuerySelection query = new QuerySelection(where, whereArgs); switch (matcher.match(uri)) { + case CALC_SUGGESTED_FROM_LATEST: + setSuggestedFromLatest(); + return 0; + + case CALC_SUGGESTED_FROM_UPSTREAM: + setSuggestedFromUpstream(); + return 0; + case CODE_SINGLE: query = query.add(querySingle(uri.getLastPathSegment())); break; @@ -565,4 +598,102 @@ public class AppProvider extends FDroidProvider { return count; } + /** + * Look at the upstream version of each app, our goal is to find the apk + * with the closest version code to that, without going over. + * If the app is not compatible at all (i.e. no versions were compatible) + * then we take the highest, otherwise we take the highest compatible version. + * + * Replaces the existing Java code: + * + * if (app.upstreamVercode > 0) { + * int latestcode = -1; + * for (Apk apk : apksForApp) { + * if ((!app.compatible || apk.compatible) + * && apk.vercode <= app.upstreamVercode + * && apk.vercode > latestcode) { + * latestApk = apk; + * latestcode = apk.vercode; + * } + * } + * } + * + * And it can be read a little easier like this (without the string concats): + * + * UPDATE fdroid_app + * SET suggestedVercode = ( + * SELECT MAX(fdroid_apk.vercode) + * FROM fdroid_apk + * WHERE + * fdroid_app.id = fdroid_apk.id AND + * fdroid_apk.vercode <= fdroid_app.upstreamVercode AND + * ( fdroid_app.compatible = 0 OR fdroid_apk.compatible = 1 ) + * ) + * WHERE upstreamVercode > 0 + */ + private void setSuggestedFromUpstream() { + + final String apk = DBHelper.TABLE_APK; + final String app = DBHelper.TABLE_APP; + + String updateSql = + "UPDATE " + app + + " SET suggestedVercode = ( " + + " SELECT MAX( " + apk + ".vercode ) " + + " FROM " + apk + + " WHERE " + + app + ".id = " + apk + ".id AND " + + apk + ".vercode <= " + app + ".upstreamVercode AND " + + " ( " + app + ".compatible = 0 OR " + apk + ".compatible = 1 ) ) " + + " WHERE upstreamVercode > 0 "; + + write().execSQL(updateSql); + } + + /** + * For all apps that don't specify an upstream version code, we take the + * latest apk in the repo. If the app is not compatible at all (i.e. no versions + * were compatible) then we take the highest, otherwise we take the highest + * compatible version. + * + * Replaces the existing Java code: + * + * for (Apk apk : apksForApp) { + * if ((!app.compatible || apk.compatible) + * && apk.vercode > latestCode) { + * latestApk = apk; + * latestCode = apk.vercode; + * } + * } + * + * And it can be read a little easier like this (without the string concats): + * + * UPDATE fdroid_app + * SET suggestedVercode = ( + * SELECT MAX(fdroid_apk.vercode) + * FROM fdroid_apk + * WHERE + * fdroid_app.id = fdroid_apk.id AND + * ( fdroid_app.compatible = 0 OR fdroid_apk.compatible = 1 ) + * ) + * WHERE upstreamVercode = 0 OR upstreamVercode IS NULL; + */ + private void setSuggestedFromLatest() { + + final String apk = DBHelper.TABLE_APK; + final String app = DBHelper.TABLE_APP; + + String updateSql = + "UPDATE " + app + + " SET suggestedVercode = ( " + + " SELECT MAX( " + apk + ".vercode ) " + + " FROM " + apk + + " WHERE " + + app + ".id = " + apk + ".id AND " + + " ( " + app + ".compatible = 0 OR " + apk + ".compatible = 1 ) ) " + + " WHERE upstreamVercode = 0 OR upstreamVercode IS NULL "; + + write().execSQL(updateSql); + } + } From 628d684ab98cc5cc7e2f79999a6b67f4c287b64e Mon Sep 17 00:00:00 2001 From: F-Droid Translatebot Date: Thu, 3 Apr 2014 20:57:36 +0100 Subject: [PATCH 239/282] Translation updates --- res/values-fa/strings.xml | 11 +++++++++++ res/values-hu/array.xml | 22 +++++++++++----------- res/values-sv/strings.xml | 10 ++++++++++ 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml index 9a6fa1356..9d0de06d1 100644 --- a/res/values-fa/strings.xml +++ b/res/values-fa/strings.xml @@ -7,13 +7,23 @@ به‌نظر می‌رسد این بسته با دستگاه شما هماهنگ نیست. آیا می‌خواهید آن را به هر قیمتی آزمایش و نصب کنید؟ شما در حال قدیمی‌کردن و کاهش درجهٔ این برنامه هستید. انجام چنین کاری ممکن منجر به خرابی یا از دست رفتن داده‌های شما شود. آیا می‌خواهید سعی کنید این برنامه را به هر قیمتی قدیمی کنید؟ نسخه + ویرایش + حذف + فعال‌سازی ارسال ان‌اف‌سی... میانگیری برنامه‌های دریافت‌شده + نگه‌داشتن پرونده‌های ای‌پی‌کی بر روی کارت اس‌دی + هیچ پروندهٔ ای‌پی‌کی را نگه‌ندار به‌روزرسانی‌ها دیگر آخرین اسکن مخزن: %S هیچ‌گاه + به‌روزرسانی دوره‌ای خودکار + به‌روزرسانی‌نکردن خودکار فهرست برنامه‌ها فقط هنگام اتصال وای‌فای + به‌روزرسانی فهرست برنامه‌ها خودکار فقط با وای‌فای + همیشه فهرست برنامه‌ها را خودکار به‌روز کن مطلع‌سازی + آگاهی‌دادن هنگامی که به‌روزرسانی‌ها موجود هستند به‌روزرسانی تاریخچه جستجوی نتایج مشخصات برنامه @@ -59,6 +69,7 @@ جستجو مخزن جدید حذف مخزن + یافتن مخازن محلی اجرا به اشتراک‌گذاری نصب diff --git a/res/values-hu/array.xml b/res/values-hu/array.xml index a27da5898..8d2f806a0 100644 --- a/res/values-hu/array.xml +++ b/res/values-hu/array.xml @@ -1,14 +1,14 @@ - - Soha - Óránként - 4 óránként - 12 óránként - Naponta - - - Sötét - Világos - + + Soha + Óránként + 4 óránként + 12 óránként + Naponta + + + Sötét + Világos + diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index eb3143a9a..63ce10f23 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -63,6 +63,8 @@ En förrådsadress ser ut så här: https://f-droid.org/repo Var vänlig vänta Uppdaterar programlistan… Hämtar program från + NFC är inte aktiverat! + Gå till NFC-inställningar… Förrådsadress fingeravtryck (valfritt) Detta förråd existerar redan! @@ -79,6 +81,7 @@ Vill du uppdatera dem? Sök Nytt förråd Ta bort förråd + Hitta lokala förråd Kör Dela Installera @@ -118,6 +121,8 @@ Vill du uppdatera dem? Alla Nyheter Nyligt uppdaterade + Lokala FDroid-förråd + Upptäcker lokala FDroid-förråd… Hämtar %2$s / %3$s (%4$d%%) från %1$s @@ -127,6 +132,7 @@ Vill du uppdatera dem? Ansluter till %1$s Kontrollerar appars kompatibilitet med din enhet… + Sparar programdetaljer (%1$d%%) Inga behörigheter används. Behörigheter för version %s Visa behörigheter @@ -152,4 +158,8 @@ Vill du uppdatera dem? Då ett förråd tas bort kommer inte längre appar därifrån vara tillgängliga via F-Droid. Obs: Alla tidigare installerade appar kommer finnas kvar på din enhet. Avaktiverade \"%1$s\". Du kommer behöva återaktivera detta förråd för att kunna installera appar från det. %s eller senare + upp till %s + %1$s upp till %2$s + Din enhet är inte på samma trådlösa nätverk som det lokala förråd du just lagt till! Försök ansluta till detta nätverk: %s + Kräver: %1$s From 793bd618ac9e3d598a4affd4d1d1a858f7711006 Mon Sep 17 00:00:00 2001 From: AlexanderR Date: Fri, 4 Apr 2014 18:06:03 +1100 Subject: [PATCH 240/282] Fixed weird crash on emulator --- src/org/fdroid/fdroid/CompatibilityChecker.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/org/fdroid/fdroid/CompatibilityChecker.java b/src/org/fdroid/fdroid/CompatibilityChecker.java index c17565120..f750f971a 100644 --- a/src/org/fdroid/fdroid/CompatibilityChecker.java +++ b/src/org/fdroid/fdroid/CompatibilityChecker.java @@ -34,11 +34,13 @@ public class CompatibilityChecker extends Compatibility { logMsg.append("Available device features:"); features = new HashSet(); if (pm != null) { - for (FeatureInfo fi : pm.getSystemAvailableFeatures()) { - features.add(fi.name); - logMsg.append('\n'); - logMsg.append(fi.name); - } + final FeatureInfo[] featureArray = pm.getSystemAvailableFeatures(); + if (featureArray != null) + for (FeatureInfo fi : pm.getSystemAvailableFeatures()) { + features.add(fi.name); + logMsg.append('\n'); + logMsg.append(fi.name); + } } cpuAbis = SupportedArchitectures.getAbis(); From c1daa996170659d7b5b47736faee60d8e3f9cbf2 Mon Sep 17 00:00:00 2001 From: AlexanderR Date: Sat, 5 Apr 2014 10:32:58 +1100 Subject: [PATCH 241/282] Use managedQuery to let Search Activity automatically dispose of it's Cursor. Should be somehow done via Loaders someday. --- src/org/fdroid/fdroid/SearchResults.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/SearchResults.java b/src/org/fdroid/fdroid/SearchResults.java index f2a1d5357..01adb01b1 100644 --- a/src/org/fdroid/fdroid/SearchResults.java +++ b/src/org/fdroid/fdroid/SearchResults.java @@ -47,6 +47,7 @@ public class SearchResults extends ListActivity { private static final int SEARCH = Menu.FIRST; + private Cursor cursor; private AppListAdapter adapter; protected String getQuery() { @@ -107,10 +108,12 @@ public class SearchResults extends ListActivity { if (query == null || query.length() == 0) finish(); - Cursor cursor = getContentResolver().query( + if (cursor != null) cursor.close(); + cursor = managedQuery( AppProvider.getSearchUri(query), AppListFragment.APP_PROJECTION, null, null, AppListFragment.APP_SORT); + TextView tv = (TextView) findViewById(R.id.description); String headertext; int count = cursor != null ? cursor.getCount() : 0; From 04ea93cce8b83b6b43a26fa0639b0aa71d5dae89 Mon Sep 17 00:00:00 2001 From: AlexanderR Date: Sat, 5 Apr 2014 17:08:29 +1100 Subject: [PATCH 242/282] Category list i18n --- res/values-ru/strings.xml | 16 ++++++++++++++++ res/values/strings.xml | 16 ++++++++++++++++ .../views/fragments/AvailableAppsFragment.java | 13 ++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 057c9d14b..d67c37962 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -155,4 +155,20 @@ \"%1$s\" отключен. Вам нужно повторно включить этот репозиторий для установки приложений из него. %s или позднее до %s + + Детские + Разработка + Игры + Интернет + Математика + Мультимедиа + Навигация + Новости + Офис + Связь + Чтение + Научные + Безопасность + Системные + Обои diff --git a/res/values/strings.xml b/res/values/strings.xml index 6f15f3a5e..0d0861e7c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -216,4 +216,20 @@ App icon Repo icon + Children + Development + Games + Internet + Mathematics + Multimedia + Navigation + News + Office + Phone & SMS + Reading + Science & Education + Security + System + Wallpaper + diff --git a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java index 98f70d35a..b7f452d53 100644 --- a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java @@ -3,6 +3,7 @@ package org.fdroid.fdroid.views.fragments; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; +import android.content.res.Resources; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; @@ -20,6 +21,7 @@ import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.views.AppListAdapter; import org.fdroid.fdroid.views.AvailableAppListAdapter; +import java.util.ArrayList; import java.util.List; public class AvailableAppsFragment extends AppListFragment implements @@ -86,13 +88,22 @@ public class AvailableAppsFragment extends AppListFragment implements final List categories = AppProvider.Helper.categories(getActivity()); + // attempt to translate category names with fallback to default name + List translatedCategories = new ArrayList<>(categories.size()); + Resources res = getResources(); + for (String category:categories) + { + int id = res.getIdentifier(category.replace(" & ", "_"), "string", getActivity().getPackageName()); + translatedCategories.add(id == 0 ? category : getString(id)); + } + categorySpinner = new Spinner(getActivity()); // Giving it an ID lets the default save/restore state // functionality do its stuff. categorySpinner.setId(R.id.categorySpinner); ArrayAdapter adapter = new ArrayAdapter( - getActivity(), android.R.layout.simple_spinner_item, categories); + getActivity(), android.R.layout.simple_spinner_item, translatedCategories); adapter.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item); categorySpinner.setAdapter(adapter); From dcf3a9dae8c0c26eb9a24c49ada392da6ae05da9 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Sat, 5 Apr 2014 17:19:49 +0000 Subject: [PATCH 243/282] Fixed assumption that repo updates have all apps available. Previously, I accidentally made the repo updater presume that it had access to all apps in a big fat list. This meant that I was iterating over that list, performing calculations, etc, rather than actually querying the entire database. The solution was to bundled all update-service related processing to one process in AppProvider (I didn't want to have to pull every single app/apk out of the database in order to perform the update, because that will become more and more burdensom as the repo grows). There is a method calcDetailsFromIndex() in the AppProvider.Helper class. It now does three things: * updates compatibility flag. * updates suggested version (outstanding issue is documented in gitlab issue #1) * updates iconsUrl (fixed in this commit) Icons from old repos will just have icons in an "icons" dir in the same folder as the index.jar. New repos have density specific icon dirs (e.g. "icons-240") which depend on the device F-Droid is running on. --- src/org/fdroid/fdroid/UpdateService.java | 60 +++------ src/org/fdroid/fdroid/data/AppProvider.java | 134 ++++++++++++++++---- 2 files changed, 125 insertions(+), 69 deletions(-) diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index 7231b99a6..201ab8d79 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -43,7 +43,6 @@ public class UpdateService extends IntentService implements ProgressListener { public static final String RESULT_MESSAGE = "msg"; 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; @@ -309,11 +308,11 @@ public class UpdateService extends IntentService implements ProgressListener { List listOfAppsToUpdate = new ArrayList(); listOfAppsToUpdate.addAll(appsToUpdate.values()); - calcCompatibilityFlags(this, apksToUpdate, appsToUpdate); - calcIconUrls(this, apksToUpdate, appsToUpdate, repos); + calcApkCompatibilityFlags(this, apksToUpdate); // Need to do this BEFORE updating the apks, otherwise when it continually - // calls "get apks for repo X" then it will be getting the newly created apks + // calls "get existing apks for repo X" then it will be getting the newly + // created apks, rather than those from the fresh, juicy index we just processed. removeApksNoLongerInRepo(apksToUpdate, updatedRepos); int totalInsertsUpdates = listOfAppsToUpdate.size() + apksToUpdate.size(); @@ -321,7 +320,13 @@ public class UpdateService extends IntentService implements ProgressListener { updateOrInsertApks(apksToUpdate, totalInsertsUpdates, listOfAppsToUpdate.size()); removeApksFromRepos(disabledRepos); removeAppsWithoutApks(); - AppProvider.Helper.calcSuggestedVersionsForAll(this); + + // This will sort out the icon urls, compatibility flags. and suggested version + // for each app. It used to happen here in Java code, but was moved to SQL when + // it became apparant we don't always have enough info (depending on which repos + // were updated). + AppProvider.Helper.calcDetailsFromIndex(this); + notifyContentProviders(); if (prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, false)) { @@ -358,8 +363,13 @@ public class UpdateService extends IntentService implements ProgressListener { getContentResolver().notifyChange(ApkProvider.getContentUri(), null); } - private static void calcCompatibilityFlags(Context context, List apks, - Map apps) { + /** + * This cannot be offloaded to the database (as we did with the query which + * updates apps, depending on whether their apks are compatible or not). + * The reason is that we need to interact with the CompatibilityChecker + * in order to see if, and why an apk is not compatible. + */ + private static void calcApkCompatibilityFlags(Context context, List apks) { CompatibilityChecker checker = new CompatibilityChecker(context); for (Apk apk : apks) { List reasons = checker.getIncompatibleReasons(apk); @@ -369,42 +379,6 @@ public class UpdateService extends IntentService implements ProgressListener { } else { apk.compatible = true; apk.incompatible_reasons = null; - apps.get(apk.id).compatible = true; - } - } - } - - private static void calcIconUrls(Context context, List apks, - Map apps, List repos) { - String iconsDir = Utils.getIconsDir(context); - Log.d("FDroid", "Density-specific icons dir is " + iconsDir); - for (App app : apps.values()) { - if (app.iconUrl == null && app.icon != null) { - calcIconUrl(iconsDir, app, apks, repos); - } - } - } - - private static void calcIconUrl(String iconsDir, App app, - List allApks, List repos) { - List apksForApp = new ArrayList(); - for (Apk apk : allApks) { - if (apk.id.equals(app.id)) { - apksForApp.add(apk); - } - } - - Collections.sort(apksForApp); - for (int i = apksForApp.size() - 1; i >= 0; i --) { - Apk apk = apksForApp.get(i); - for (Repo repo : repos) { - if (repo.getId() != apk.repo) continue; - if (repo.version >= Repo.VERSION_DENSITY_SPECIFIC_ICONS) { - app.iconUrl = repo.address + iconsDir + app.icon; - } else { - app.iconUrl = repo.address + "/icons/" + app.icon; - } - return; } } } diff --git a/src/org/fdroid/fdroid/data/AppProvider.java b/src/org/fdroid/fdroid/data/AppProvider.java index a76046bea..9bd19d476 100644 --- a/src/org/fdroid/fdroid/data/AppProvider.java +++ b/src/org/fdroid/fdroid/data/AppProvider.java @@ -7,7 +7,6 @@ import android.net.Uri; import android.util.Log; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; import java.util.*; @@ -125,12 +124,9 @@ public class AppProvider extends FDroidProvider { * content provider class, so I've hidden the implementation of this (by making it private) in case * I find a better way in the future. */ - public static void calcSuggestedVersionsForAll(Context context) { - Uri fromUpstream = calcSuggestedVersionFromUpstream(); + public static void calcDetailsFromIndex(Context context) { + Uri fromUpstream = calcAppDetailsFromIndexUri(); context.getContentResolver().update(fromUpstream, null, null, null); - - Uri fromLatest = calcSuggestedVersionFromLatest(); - context.getContentResolver().update(fromLatest, null, null, null); } } @@ -246,8 +242,8 @@ public class AppProvider extends FDroidProvider { private static final String PATH_NEWLY_ADDED = "newlyAdded"; private static final String PATH_CATEGORY = "category"; private static final String PATH_IGNORED = "ignored"; - private static final String PATH_CALC_SUGGESTED_FROM_UPSTREAM = "calcSuggestedFromUpstream"; - private static final String PATH_CALC_SUGGESTED_FROM_LATEST = "calcSuggestedFromLatest"; + + private static final String PATH_CALC_APP_DETAILS_FROM_INDEX = "calcDetailsFromIndex"; private static final int CAN_UPDATE = CODE_SINGLE + 1; private static final int INSTALLED = CAN_UPDATE + 1; @@ -258,13 +254,12 @@ public class AppProvider extends FDroidProvider { private static final int NEWLY_ADDED = RECENTLY_UPDATED + 1; private static final int CATEGORY = NEWLY_ADDED + 1; private static final int IGNORED = CATEGORY + 1; - private static final int CALC_SUGGESTED_FROM_UPSTREAM = IGNORED + 1; - private static final int CALC_SUGGESTED_FROM_LATEST = CALC_SUGGESTED_FROM_UPSTREAM + 1; + + private static final int CALC_APP_DETAILS_FROM_INDEX = IGNORED + 1; static { matcher.addURI(getAuthority(), null, CODE_LIST); - matcher.addURI(getAuthority(), PATH_CALC_SUGGESTED_FROM_UPSTREAM, CALC_SUGGESTED_FROM_UPSTREAM); - matcher.addURI(getAuthority(), PATH_CALC_SUGGESTED_FROM_LATEST, CALC_SUGGESTED_FROM_LATEST); + matcher.addURI(getAuthority(), PATH_CALC_APP_DETAILS_FROM_INDEX, CALC_APP_DETAILS_FROM_INDEX); matcher.addURI(getAuthority(), PATH_IGNORED, IGNORED); matcher.addURI(getAuthority(), PATH_RECENTLY_UPDATED, RECENTLY_UPDATED); matcher.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED); @@ -293,12 +288,8 @@ public class AppProvider extends FDroidProvider { return Uri.withAppendedPath(getContentUri(), PATH_IGNORED); } - private static Uri calcSuggestedVersionFromUpstream() { - return Uri.withAppendedPath(getContentUri(), PATH_CALC_SUGGESTED_FROM_UPSTREAM); - } - - private static Uri calcSuggestedVersionFromLatest() { - return Uri.withAppendedPath(getContentUri(), PATH_CALC_SUGGESTED_FROM_LATEST); + private static Uri calcAppDetailsFromIndexUri() { + return Uri.withAppendedPath(getContentUri(), PATH_CALC_APP_DETAILS_FROM_INDEX); } public static Uri getCategoryUri(String category) { @@ -575,12 +566,8 @@ public class AppProvider extends FDroidProvider { QuerySelection query = new QuerySelection(where, whereArgs); switch (matcher.match(uri)) { - case CALC_SUGGESTED_FROM_LATEST: - setSuggestedFromLatest(); - return 0; - - case CALC_SUGGESTED_FROM_UPSTREAM: - setSuggestedFromUpstream(); + case CALC_APP_DETAILS_FROM_INDEX: + updateAppDetails(); return 0; case CODE_SINGLE: @@ -598,6 +585,13 @@ public class AppProvider extends FDroidProvider { return count; } + private void updateAppDetails() { + updateCompatibleFlags(); + updateSuggestedFromLatest(); + updateSuggestedFromUpstream(); + updateIconUrls(); + } + /** * Look at the upstream version of each app, our goal is to find the apk * with the closest version code to that, without going over. @@ -631,7 +625,9 @@ public class AppProvider extends FDroidProvider { * ) * WHERE upstreamVercode > 0 */ - private void setSuggestedFromUpstream() { + private void updateSuggestedFromUpstream() { + + Log.d("FDroid", "Calculating suggested versions for all apps which specify an upstream version code."); final String apk = DBHelper.TABLE_APK; final String app = DBHelper.TABLE_APP; @@ -650,6 +646,33 @@ public class AppProvider extends FDroidProvider { write().execSQL(updateSql); } + /** + * For each app, we want to set the isCompatible flag to 1 if any of the apks we know + * about are compatible, and 0 otherwise. + * + * Here is the SQL query without all of the concatenations (hopefully it's a bit easier to read): + * + * UPDATE fdroid_app SET compatible = ( + * SELECT TOTAL( fdroid_apk.compatible ) > 0 + * FROM fdroid_apk + * WHERE fdroid_apk.id = fdroid_app.id ); + */ + private void updateCompatibleFlags() { + + Log.d("FDroid", "Calculating whether apps are compatible, based on whether any of their apks are compatible"); + + final String apk = DBHelper.TABLE_APK; + final String app = DBHelper.TABLE_APP; + + String updateSql = + "UPDATE " + app + " SET compatible = ( " + + " SELECT TOTAL( " + apk + ".compatible ) > 0 " + + " FROM " + apk + + " WHERE " + apk + ".id = " + app + ".id );"; + + write().execSQL(updateSql); + } + /** * For all apps that don't specify an upstream version code, we take the * latest apk in the repo. If the app is not compatible at all (i.e. no versions @@ -678,7 +701,9 @@ public class AppProvider extends FDroidProvider { * ) * WHERE upstreamVercode = 0 OR upstreamVercode IS NULL; */ - private void setSuggestedFromLatest() { + private void updateSuggestedFromLatest() { + + Log.d("FDroid", "Calculating suggested versions for all apps which don't specify an upstream version code."); final String apk = DBHelper.TABLE_APK; final String app = DBHelper.TABLE_APP; @@ -696,4 +721,61 @@ public class AppProvider extends FDroidProvider { write().execSQL(updateSql); } + private void updateIconUrls() { + + Log.d("FDroid", "Updating icon paths for apps belonging to repos with version >= " + Repo.VERSION_DENSITY_SPECIFIC_ICONS); + String iconsDir = Utils.getIconsDir(getContext()); + String repoVersion = Integer.toString(Repo.VERSION_DENSITY_SPECIFIC_ICONS); + String query = getIconUpdateQuery(); + String[] params = { iconsDir, repoVersion }; + write().execSQL(query, params); + } + + /** + * Returns a query which requires two parameters to be bound. These are (in order): + * 1) The repo version that introduced density specific icons + * 2) The dir to density specific icons for the current device. + */ + private String getIconUpdateQuery() { + + final String apk = DBHelper.TABLE_APK; + final String app = DBHelper.TABLE_APP; + final String repo = DBHelper.TABLE_REPO; + + return + " UPDATE " + app + " SET iconUrl = ( " + + " SELECT " + + + // Concatenate (using the "||" operator) the address, the icons directory (bound to the ? as the + // second parameter when executing the query) and the icon path. + " ( " + + repo + ".address " + + " || " + + + // If the repo has the relevant version, then use a more intelligent icons dir, + // otherwise revert to '/icons/' + " CASE WHEN " + repo + ".version >= ? THEN ? ELSE '/icons/' END " + + + " || " + + app + ".icon " + + ") " + + " FROM " + + apk + + " JOIN " + repo + " ON (" + repo + "._id = " + apk + ".repo) " + + " WHERE " + + app + ".id = " + apk + ".id AND " + + apk + ".vercode = ( " + + + // We only want the latest apk here. Ideally, we should instead join + // onto apk.suggestedVercode, but as per https://gitlab.com/fdroid/fdroidclient/issues/1 + // there may be some situations where suggestedVercode isn't set. + // TODO: If we can guarantee that suggestedVercode is set, then join onto that instead. + // This will save from doing a futher sub query for each app. + " SELECT MAX(inner_apk.vercode) " + + " FROM fdroid_apk as inner_apk " + + " WHERE inner_apk.id = fdroid_apk.id ) " + + " AND fdroid_apk.repo = fdroid_repo._id " + + " ) "; + } + } From 34d7172ad62aff64d1655974d8bfbd3187cce8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 4 Apr 2014 17:40:24 +0200 Subject: [PATCH 244/282] Bump support lib from 19.0 to 19.1 --- libs/README.android-support-v4.txt | 4 ++-- libs/android-support-v4.jar | Bin 629518 -> 648327 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/README.android-support-v4.txt b/libs/README.android-support-v4.txt index 8c280042c..f1fb7995c 100644 --- a/libs/README.android-support-v4.txt +++ b/libs/README.android-support-v4.txt @@ -1,5 +1,5 @@ -The Android support library v4 is currently at *revision 19* from the Android SDK. -This reversion was released on October 2013. +The Android support library v4 is currently at *revision 19.1* from the Android SDK. +This reversion was released on March 2014. See NOTICE.android-support-v4.txt for license. See http://developer.android.com/tools/extras/support-library.html for further info. diff --git a/libs/android-support-v4.jar b/libs/android-support-v4.jar index 7dad0a7425860552ad436fa48089deb7593edb21..187bdf48b1dffd93f92d8abbe656dbba29f8b4cd 100644 GIT binary patch delta 540197 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%0Aox0{YI3k+0dt9JDIuynXSl`e%PjH69 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�ht6pmm~Rz*mQ>EX9<8oj%c&Zia|x@d79Zz4 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%0D-cai!k5~Ao_p$x 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�+ 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

N=W#Tg(n9ti<#NH+A+h~s@ zj46lEzd`h>@zl)4rLQ-rZmii$-%R-UAqVZB^QZbj@VsDS-{G7D>KcoUs1T#e^vZa? zZPP2g^I@<7c@9!#27M&V^h(7(T@J&~f_B>7ildwM-rJA?!yaR2EtA|HVPad$;y|(? zf@031Dj&$7Wq2nseJc@TlP7;d*qUm?r1;O*3#}^Ucq6h7>KOp#HTy%JyMU*7``2ur zRNFxiH{{-zaXyy}Lp4Dm4s!?&%Uk4p73-s^yIHi-+#T!D5A$r~38DQImP{Sq04aBN z)x7|3rnh7GEO`(;2}?H=>)k?jN5dp8rm+m|6L#QS1|kdefq01t*y^;-d$wzemZMbW zRGlPkGij1Gs%^1uQ9s$MBtBOPiD9NJxHfT$OTKIPUmiLX5SH>n*QC=@7n$}&>8z<> zq)AaBq~-o3EkkL#T31b&rzrRMK~mGZ$miUE=vn{Eq{ScVgUCkoqFY+egKzjhVZZmLQV5eFBFwg z_CPxxHoF8O+4Eze31J6XG6?_#qVwS1sHvVMd|SD@6h67|uT!d&uE_!jHc z5Q_Z`6gu42Vr`qZbhG z-HRv4jl=qg$2{e#j(&`qdgg-`hTZVRJ@Ar6`O9al9i%={y|db} zC=ay5-FQ8uU95ZLX@KY6wYVc`E0+$)75?8F9MM;P;rs8#6o(I$5l^jhc2UtSi@nw+#j# zl|h`$8Jy6lNlu?$9ALuUUDiesK+^g_c+2rIq3xSjLXAmpEl}^oi zok{xL?k}O^3&anVL)!7Ec|^0XNqu%W0_f4$*DlCL@B%gd$qo`3!0;R6!0i%puvSq^n~EBvUJW;VB19F_AC7 z4oyTAjkKZ@k-R?;xje1~P_9+L?)-)EM68I!JJa8D+J_ryjg{dm_Hn{l+Eg?gY6m(L zj3;1s3$^GZ8x4J+HKok`v-Jg9h8fklJEe|la=#lBbhi+d%MqAQ*&E>xU+hz6?8i}K zn`FfKr-M2UptA4^?Vo0wY}m>426P+sH7408E4vzNWFi1bz5*;*wa!gI224Z`8vy5Q zyp8W4AlO>GlNh!}3<5Z|%~nQZ6x65;HD9*O(o|H6-XyhDYs}R3@xQZ)+Dl1>9!)5N z9P`)D7|X+aQ0qtG6C%3hBt6P0%6I>MRPdRE+GOwiEX8U_Ioe&}z4wG5mTc0SvF%wtL5)ZB8*Xx0!Bf zx0P<}L4*!}bu_n$?lZT(?#^Y)ZP^Z!ZEGF2Ys4yxW33_+_zs%9tI)xx8$Um!8$ z`W`&H@>|W}rrcU5 zS~~Zw+PYtZB66A#YnqHsuEoij3~)o2gM;29r}4cbG&1Lp?Ccsoa>8E+iuE}L`mcv! znj=nQ75G2lrr|+ZIPwkLR(85NZi1?Y#(6)|rI6^$pMlS<(<*pSizs8I5o=w1CRi8S zbxwN)#_gHvMqLNM((=5AK)N}WtY)EVj)Qfj5ky4v@JrEE)j8wghZpIFl{Vu8YCkqZ zF89s(l{?j0^1iK1d$uu~Qkw8r049a;XgN^;BtQMkZ}jyv!uO{$$UWoFHZr?D5*MMi zp7ojLJMZ}sbx9WXJvFBr=ekIJ+2zTrH9OCXw{Lr>ouVf&3a`;&_d672E>gFE@148n zj=TzwSwMKqd})T1H0h)Z@k5-fG2Mye0wh{kTtLm>M{8{z1r5G=oZv^?kjTr$BB4Y@i zJ|-;L``-82QV%jVxo#~w#5^~^0V$qAEK^=l9qZd_(`P3z3NF=w$cSL@+uPwxryW-$ zfPxSB#~R~|{IGAMYIzLY$L|f+(p=4NG|<8#r|tU?E7ak28-W+rq8|R3{#zEw&$Lip ze9vg-Y251fP$vPN-^f_kT-COIG}D!cO+>g(it#$3+3g{ida&hZzISf{=luM8*4Q{ZsKe&6vW#iNVa2 z%Su&i+M-eQrfHFyaDEXC2@|4!&B#Y{2$2fx8vB$#tyW?osq5zK-8aWldV~CLqeggsYVj5mb zE^DFvORYoi@;zH(7`7SUE=%DpCw#ZHEGnne5_8}O)m1ZBhrX_1m@oS*J4Zq7LQ(Us~!vKT~Z%b5-53StkkBrM^TkM7%`n z`w-;y-SB6@>(2V|2w97SRJIJP-u(rUwGbYyt}y2l+$9)2_+Sidn5!K({t|+KqLU@F z7MJJN_24z4ye9Bmk7FZMTl9d&N0*VHt-t>ajCE2wmh9ZBK@VJeFdmmmtu4uvELKiJ z+pu#}kuS~{rlj)mG6W22S)WL1UpbGfDD|Arq=c7Hee+wTwv2|EQJ28!Hm#+g2&2LL za60L&b(>YhQfi)w&w@*1+F9^$oQ5Xj*_xz5=QVaK2!?@-yWx@!*!S@9f|CYna~#KS1dZyNUUmR_^x1_4P%%#G z0dLv3iN+yfQQ-05mJGB>HO%xpab8S!vimg0C1oTK8re*ER2SjH(N83*w__OM3yMFnv?Y?j2!F2#{hmr;4~Q(I)G%e)Cu-o zpj5pnJLqZ6KzI(VhyjEE-ZGe;-h_)VMRcsR^-7_tTwFWMn6$L{o^lZd{^oVxK<9nvKwYwN921p#{a?$`(R4-z zUVjMDs*%O%Ea0qBE9y<^NJ?!ej1@aoii*lw|LiQ4j*5P?%5aA^M_#yIniUqQgsPH% zJCC~vKTv0tSYi~fG^eUTgQNHk0*^l5YUa9#L0iL&TDWpdria72Un+|vg{@)F0D-8_ za96Kr1)$+yh?^9?1Ma0GOv7KcgTh<3!{jC0bFXMv3EZ_u*y=~aRWpk8Kddbd4yItr z*`eaC+`-{3-{E57jkYa)ZVJY_E(yaLc$CJ~8>GS28~9HcTd+5jt+^Z8mZ}0DZ7i!7 z(v}&iaoGy4k8d+}R4ya^NQ(WSa37NWZ+L;(Yom9OKKYxb;Ow0vTpvGui|0h&OM;H_ z-6dQfV8B+{b7t@s+y$_kNHaosz~UM0vtozgxjgJ2;F;$$cPIN9>kQEUlW8pJxOULX z6EL4CMy{V92DEfTx}r&W6U5`hbHTyXJSjBJs^L|$y0a3dUq0ludgU$YJ4innugz2g zFB&@Lh$rI+;o4E0ikWTiBX#Lz&x$W1WavTziiq>EgttfAP3I38_7>mL)uvgfVUNfp z1Je0S*PWKjJ9ZlVYlF2dt@`4$?TuQg+Wrk;LKEQ9FKvt&sHuNC{i#Mhvb4GnqY|tQ z@DS$-75F*%Yv%Dz+xoWSV&Z9AHQ%4Ft!cKe+ZHEuKKLvCSV^4xsR$KjP7^=6n7s}Y zSUC8Y!1;>A&W_WGqZS$)VPE`rA}RFWV&{d^cVJ=@rDAxr_=4m}5f$RPvx&d{A%QYw zrah$mUwj}bZ%1Xc6XpmWn0L{^0gf4hXX2Ulyq><+kZ?1Y+^F|}Asi9~x;l5-zY4n> z09-Oxb4Yz8;S&8*ma-Egv)Im;L)8)^kh||shAv`c?39zf?fP`-@YLffl|>8pRXs2A zi&VemGmcEg`1*z4f=;ipQ+Wx_XA)JtUb8xGddXONLbOr}oF-}lhgtiY(;^Z;#l3kk z>`b$0X!2Wl)cHeI(l-2FU^J|UFL9P+-9VO!B2s^*jcOv0*Md)_k%x%;`*Xaq5osAIjoyb`m3ZyqJgo(%RPN$0sfInXPDL%Ay&H z7rZJB3D~u$%Z==SGWOEA*-toGPFeo|HHQnw)S|G`F;*Ktj(vjPCZzilKtnIROS(&y z5ksM;$)~MIx4IHc;mCQ+5kte~>DjmeQhJMal6F}B%X!R1%v zAW7^=V~IU*MnU=b`&-)kULjb-FXrX(9P0rwox#-Z!bD2h!z<(9Ug0Y0451S}KOvCv z3$BX(bJo*}oFwHEbvw#qV6GPjxO}|)_7cV~Ew~O7-%-K+*Q>ENh8>)&>ta#9lK-h$ACN`kXH;=2V6}T`U$p`q)o`OD&b_!t!lt^+_zI za1=qL180fqj`b@VH&L*H2(_j1m}}`ygu8yDA7}%@{R~ePC_98iz}wk<3$8`h5Dn1N zoQDZoVq~BI{g4~6RtSv-y!vc;`Ha6MBhpgc-)ghiO@)0y%btKmf2c|bBrP~XeBJAu ze+szV0C3fSAC5ni`$@v7Hf#n)j|0QDE8}5p4ZSyIz+7vXZ3X#kXsGjp{h1Jgn6;+% z9TzHzBcFj<4_2T_z=0mRX5oj+z$M3{n9&a5Fe|4}IO1}9NJouWmK{}yBYV?&?fiHL zt|n2yl*wFVN-L4_3gGn4mT<5I!;qUNJoqvx4d5mBgs+@k-bKJ^BcUj+oh78IH|WE9 z>n<@7QkNo_Rz%BBtIRcxdRvEWW>c=<(v@wEQ!%XO557JDT;Q9o3AfuA6LTTzZOO)I zO}FM4TWkukwr2R)5bu7ROAo0D^*q|05yFrg0#{Rwb3m==MPF-9_HuMg3pkqG&J@#YM!W~{&T@1QAH#QBR8CXcuZ=M7T? z*cI~>O8^}T+=yvjhw9L$+Za8HJJJXs&0`dpWK(dcoA#qZaQ9Aca|wc9n3<2L1Xqa zmHuw=Dzp`8d2!#43ud9jgp?RXWldzPnlT$=85$~kG?M*XzA47Gbbt{Q{lLm|!uT#& zP=!FZtb~u2=?6$gUyIr{?<8B7{Yi67mw%_6$Va_K5>MFB3^if)*MB7duV$NH<--2> z@%sPcOiixg0r^h^Fm&?fC^SL*N;L)uYW-EU00=o0bP2IgjPf^rVe&14|8M`s|2IJx zI^G)&3F!GpTiyN3JI-olG;)tK6pcbQk3D*{4*zJO<1Ee<--;qKzQsmq)$(Ofkw#jq z&B5>wjgdk3AG#V7%jNuTHRjR9#LC#YwuYU=1S=ZPhMGw0njY*mH|ro9ql)Xds4A0z zTBG#O9mlB2m&vDR@8~;Seea9ZqaPKqXut4>3V}cQ3H*_=OZ%C;*!y=JeNZ}3dynk7 z0%_a?k-Boq%cZ`GeQpe*W~#dBZuF&;=34!@ipj5gkY1bTCp0x&Ql!}4Cd7y>vFzWqu? z!tM(GzZxV($wH)bgPc5r*{zYX%7+_-QVgDFHY7 z+Oi+yUiq{J=At|)OPbzf#{BrllK9L9L&b&EL%At^EM{u+#=NOqE+HB(>Yq)Y^pS%1j#)zV|o+Z;klBA_{_)sOUc**|3 zBYVyQsmQTROf)-}S9Y|G*rda*b%cjl(|POfJUfrc6E}{aG4ltn)MirzGLPU-ZI9z_ z%%1fwFe=r%g3MFrc^!&EnWKnyJUpOf-`|rx1-x{vi;7+n4L)7ula#KDD$5~x?y{2& z|6A)&m0oM|xL6pFG68DnbD1Y|nTFZ%M;-m<_+L|`t`=Hz_B`(Mlce{}%{X@QTW!c^G95D!i*t-pCkVoHoxr%c1rZ4If7I4M?3@9?%`&X! zc6xk@hx#kUa^gpsOPjpd%>Bztyi=~YtBWtb4XFR6NZ?cdh{YEt(8R-<)M z>1SBaBo=b!$#dEEyW^s-WtR%{2js6A`Jq<+$hPu@v)^s^+@hYAa4<>6IjoW2>Cw8% zz7BEMYNGz$B$~Bv4=h%SQ7pjSLL8R*9~MldM`*!u%U3wEiABRs4x(f*26WAy9&We^ zK|!dCGgtkZN#y$nu~u|`3o65kcylX71g$FavrNoh6zj9JS(J+`F4SXEQrUoxb`p<3 z3gv(kba~U0qm?D9eiEL!v3#z*l0i;~3%!Zy8A}40AeHJ(=@f1~Pix>K^L!_K7LVFu zQ;FL4#~h^Wm8`F}EH|4{1Y$7{T4~ljRXz(%Iw|(ojSji;u;OXF+=T;dgD3Mz7s@Hu zNdmtnU1^17@ovlcMg8jqTMTLLc^;)@e#y^n{YPL_il&Ql=w zELU1Rg@K_NJa4%Q8C>!8PXcy1h8*#$`f0GCUY_ee3YeM>@wAAesM1MN%R;aKX&be) z`e$cpn^+UtTaJ>J(d!1XF_WO}#ZbJ!gP_o;nzv)?@_P!+VJV=rO`=DSf9PownIZ;C zs~`2)5VQ3ke3^>4p!h$>3=czed8O(%e=U;uQi^t zE`3!WzVQfa2Gz77RMfmh;56Lw+HMrTdOb1b-)?+^NMC8+ae-1YxT!csedglyvFA*g zik@joZGD?y$n?M&elOyQ^TQMv3Xk5Tia2rtHSIdFMXh6~YTFb;sYn1nohAhy?FywO zM0wHD1LbKt?LH%N!bFl)Y-N&F{2Cn~iO2sIQo3mjQQBp+D(Ae(IQqQNIGa`LeD6`) zO=Q!2$3Cbqkz693@H^X08gVpJkjq#yM9D;O3Sm!E8W9+em8GEa1uYM|DAVp+;(D8e z-(!@7KM;>ee>^4kuq?_u;lJJU^_$w8+9by`mSWLWQeEM)X+NdRUFWiPU)G@-*FJk- z)3o}}!MuvW2kyUKxIAT`r*xbl>ev!=()zQc0i)cslX>OEB7a^VflQqw9^gJ@e2s*| z95lMIp#eBAzj;pA$^?OjWih5?E0vA8R|ox4_kmOgffr#()gX7B%bjK?cY&?qzbX;B z4r0S`f+MO57>k!w_^Ug7^}C-{DmcICkCj0JD`lXOr7=F?lI_Jrf#b*0JkeFIz0N{m zM#WP~i;=~k!tATg-&6$CirkiQ4Zp}V~MhTNJp-7y|@_TVd+ z3tBHOa37|hQ^|2EP(?=XK1Oa;G1}^_;aM=T*O{(t^+hgSvCDB_g%BLOK@cEzZM~Pr z3nLCO7B!S<PPnMW7*!$Uj~xB@3|}f?#gZfSNc-PDqFhbtQIiI*{=npu9bE zQv_I6pu7{0l4@#(mdVZ5S2AS&larAOdhW7j_oS-}{NlbIGy(vH6&?M$ma?W_QoIqQ~bF-NbD>xgv>q6*LK; znAGx)3HnPx_SYCAR%h@&L(7{-D(xlBsubv~=M!1dC8xUe(xyW6vY*Cg8orR4)=&?J zi_xq%aN$EhrTB`&L1NHFvL47Z*VV)E+}{b597z*ebr#nmFMP#0T}+v7ni%>&e7$3Q zWZ$|i+#RQ*j&0j^(y?vZHY%*xwr#Ux+wRzA$H|-jzUSQg?)y9YR@M4YAF4jgHP@VT zjAuM!=*gQe)KhlqF0-#8FMY(59cltwF^BDO0=(q?vEKK$U%CMp!*Gqz(+fhsk?pBnIsX^?;s!Oz(2EW=K2VacKK_3enqyG5~nC zY95s|JVEVlj-jFhC!D&72D7h#8wF?NnljBEl+JSqYgC;*X=SH;x+Z$c#lOT639i$1 zx@-yy$fJu1?pH5lVYh)^0JlUtsv}WFm;cedMI0I}?`#9D-7|NrbXentuxPh@ z*ECg6C=TxMD(-kr_F?MYo^wCnz9~(c?fcFoyix!v2G52PI#R!v_ARoX_L<`$ZiTd| z0hDsYoRy0xqQLmXlKAA3=#+}6l~2Hbyn)#EsSIsKgziMBM|9JK&AEu6E~BbwAC;9> z=$ZJ;P#ioql$?LNqFiKCIsU;WS`ZwX<^dhi`LjJBW0AVwT>o&$GL^652aAkHrK()i zWI+nXGriM0Da}wy9jYFs!yh&|;aNbOJAjKN1E&}nwS=``L&)^8V*j}*-cw1pu`bgf zgVFyU$~VC*-33MhSGfZVSl1lkS&h-x&a>pkw(qcxM%D|gZWpL@fWmG`ym6lA+)yZq zNOGANokXuIYN$RZYStAUnDDTE{fTzwf!AV3SwsEkn!$ZGcGp^vKA-=)IuN(bM=A+H3d?fAC7tkk;cJFM&mXGrg2ffkSUV`vj14AN;tt zSLo?I@L3<-vS@aMk;sh^$*om13D~McLypnGAB5Inly}qXgQ(&LFQqe_hSK~VxMa$V z-Kd&olwbFVQ)!M1Guw4n8llq*i!jujDz>rVF+_g^aAcEAmpSA#w}{XYb(s)O9(d&z z>(9%A{`MAn`iYU$Xa$5q3q_zFh>1e#@ulonC>!|AgzQk(2dWr_W3fqf2e@(7@N<*E z-5mes(M{7wT;HnRrn*gFl_u52{OtGPFM$ZM`(1C;^EMpWiqeP24(S9C!Gb@~j3pBtSu0q3Vlx-#D5F zczY<+sK_;(l{db!gB^m0?{q1u-GS(KrdTw4n#_hRoBKJ-J>X(ifENekS?Q6Dx4+J zRXg%fuM>58znn5B1~6BWa6}v8(QbUtIUlfI4KAw3hO39Hc=Gz)sGo2y7Sgzz+Q_J~ zHcJf|7tigW3BMDG)7x=CftM`nE!}<{iPLX(pxa5%EZCM-N^8=84XtD_He);|qL*DE z(plr_epbE>hrd-NcLVI=na1SJF}waA@}+Mw%!xSt(df{;s&ZC+urcWYdc-08jOW^G zM^|Iag=c0&1Y0HIeM9A;Sk&;VaDC#;LEDzP=8f{7Mw5x&fcp5a%FhYme@3|fl6a@o z_Cx&yko;HlQk0?=J*XhchiFBGm4e70%JA|5q|MNugmEa)bB`yNrEwS54Jj`=NGSt> z_}gM(%oPgs@wWBMPG7LqtvW~V3jdpHZMF!1v5YZ8!XS#N%!I*aPk_G&s0dKnnZAFH zt=}?r?s30P!ALyksahYS zOp=st&r}DjrSme5Oz#2ra?zcjX+xpdMG{uOb}9*}H*C0HKGyViieC|~BopdD_*6Z- z?dFrib|`aMo7ksN3!Zz1l4i-+&6|ArP;ls_&vH+E@ONX{RE(C|NK{Z#mK(YQk=uWcJ(efPCv+ z7RSO(qxZ()+$*&1v23jj8n?k(X`VQBLf&6&{kF_YFdIeucQS|v`Qs)r$NVzuEkaH>mXsU(rH#wKpe=kpLV_s8Aiu-!Lo_3{259nd5mMfFkP zVcw=ntWvB}Oml`~nPI0sTA*`N^Kfp{TbF+8iUZG$xAO|}E$da=KBcR7Mu(R+7vPlB z)pDl``72h-+ABMjHU-k3K(4C)ch`MZ7iuI>SP!@~D;$9kvZQ3&eFfYjMM;X10S3^5 zuX4}vdyf{;Y6DGOkggz?RyxvRB?c1r7-=R18H{;v474Oa!txYsmN!XFE&cD(s7!5u ziYV?qF|7xVcKW}MaiO@CJ@~7c0hLpqe>rcpmoeRWa3prIXcxism^!nPra)9)sU5_b z4dApAnYJFpP`dX@=e07SbfMQ%-dU4Ur#{lmI()a?19cEDUwx1{_c2wGOcZus$s}8? z^FzTc#rgzuIa90Gdq9B)pG(#7M!Fpq@hOK8hFOPOx&D)L2=RFtmIWnF0Ar0t=7Xx7 z03UK<@B~#}N4!0rszY@@m{u^>2W11CH!zDi@~BPXk*H06AH^6>lAv}@rH>i5fIWSkZ9B(nLiu7kLpi5td-X|O-2g(|s#DPBRP zdg%c?##y5WgaO>%9mPh%45t7CuQIFHvk0xB6HcAZ@q*hOGa4fn1EykPv!FT$aHQrReScKsV)DjR~2?PB!n7>7XplcbI=h zR*CCDn&Rup=K0SA<$oJ&qo=G5LjA{Fy3*MjK_*@1lDrcn#_8`FI6FR|$<*(ed!h)vsa!#8SnN&L34Ub;k<{taP z>(rbds#3OFiMR_@GnuEIvjt{9QV&R=1|8V~fKBn();EetG`!&56BX+`KP~YwUD~KA zzIxlKpCLs&O0AcGM{R;#W7u#hU9@PeYS7*a4p;`iFCphv_3Yh$Us}H?dLx<81z#P! zYi5myDM?TD@C4T3gxv$GP_V6Ez2Z(chhY#r0X@^(@<7Me`nafslm4=IY-_PKvUxHVyv z7VPgMZDbfUTTS(z+Taq5rrOd0q-0v2ekahxzCM@o0xgf{S_&e(lQ!z<$d_DE*LsQN zDe39*_xVBPqvqrIV4uQe0h=Q7WzJ&&dl7F7jg!Vp`fq1Vj9|~ZZw2@dDbDOwhT?>> zR#*_Dc=eUSRxgDYMhvgyhWOEWxPqT+_m`xrGI%pXxmLvD7JJ85NEwXB!}@oTEI{H- z?%U|N%g-3IX>YMNlr_+-+yq4DA3kf&$le{OIR1;LbeD+fJA8P*#46)rZkf&jX~fQ} zdTa6zpWrrV4wEN-m^P{Yh^G+&QB4Ll#HmI4z$*M5QOG)#S0cm|eamrNxYLSD1qrJManp)mT5nN5Fy7KTuuBEx%-+lSo$JRvzD_uQrb zX9oYc_~ujeHG7Z$yT=UW8`RhPe`qrMuM^0B2YM>o*v-kK_|iE$J5%5)6+1c2GY;#| z<&j`%BxCF%p*iE5KKbUcpHDfiC-&W&r9H(z^**iQoA9(;%HRf(*-Z2LOgUvcUADbH zpRK!nQ{j;@g1|;obT>Af7y2%OtVJeC){Ln*TZe<D-GWU%u_wu)32! zeMp<1iO1-PwV)#H0}exi3T97QAfaW)j&SO+63^PX*S|*Rxt854xAP)$((B;;BC?ip z(CmI<%TGe?NQcyPq1q-y1TfLa{V>xf1_oQHW0-;h2#8OpL0B=7=1BMa2tVCc@dNJW z)X0t3XfF^=Uc~>R!~hBNa%9S-`iGNEai9BZss9$qasirrl92g!8&ztQmx*u6A2ojs zcUk4f_tMN^s@r3reU4UOviLx7m(J11G|9m>%O1LGKV4P1oBKoY|0_wNmE; zWmTh=>PnP9U{znj2Zk}60<*c0+@mw@CkC2>X&h%ljK8-)6=7~7(}~!1+(TEsfsS4z zb>4sj4<&|pR5N$Ltp}Hks|ZHe*k*3}2HFl_edk|}hx9&2GisnMxeKKL{$ z>gHI5ctfi~)Js|fKe6<)FL9ntuaB4%hW3RvyLL*b|+V0gc&B>dU=KcQn1jdhV4+7H zsR~0a&gs;FFN|aePFsIOI;l_{`F60A9uRy4R9&o*M#oT0KI0Os>+7F^#%wQtD^scr zo~w{;%O6vr>zDRT?r`0PUZ?S>cGk8)TYS-$RbSHI(^0?kp^rUWOC9yu+89#5FXu}^ zhP}x*8SlGQ6r(|{wH?`=oGzKFS@n6C(#;0GJV!*Y3Duw_id!$khn_a=`+HyatCO{@ zwbEN_n64CMtw*cVDy8^Ho*Sn99X!attvC(gF0&0%1-(oWp=5y(Qf>QW-;)@@-`8sK z=4N1Z(r6WuWoq1qOffI7r#XgX0nE|pbbMt~EUOkw?XwSh4ixr>mmRKaEB2*%I~_}^ zjU||nhwe7s!FVdO2NUtM8m*2XcYO_^q$`gpM0H^;b^;~NpbBuu^ryimZ+F+Vuj5*0{jh)MK&hFa*N(oTs=*Q1ShkQc>nm?@_`jgYM&})rofa+8@M#Od2zvHRJ`Q^LlwZuxG7-x^MSx3{Qhqqf!L!H(&;=9?45*50G-f- zZBWbn;u9)aw!43GI+PXx_wGuQEqK9 z0rGYhVgB|wLT{6YSXAO6fX@m(V^@%1rU0Gcc{Ii=(+wO3fFO%(4il%RlMJ^aFcu)6 zaA;3KG?}NCKvfY_iDvVVfJ#oN5wC^5JF7ZiF)1p<)fAiX+XvhHGcZymUZL2RN^?ja zH=mGLN?EKivJwqtUbQ9}QH`962){_hqzS%?_$E&Wl8v7L98YiGx`$Uq@VpDF(O`dh4B{Bg5G=GNzmL}N%C(e`uZ(f7*h%I9_S)%nrK zZ3BASaTZkoKf7id{5#8buLPNpcONCejmZN&!HwAi!LBWB)+?MLNa_)NAiRiidJer+RkQh z0KUm>-8a5yIhMVICzJ#^>L=I+{_!7|s@I(0Ip{y3zZdb_87sEa=8DQFwLmgy(v=*6 zwHu|U)zcEA&XSQ6_a_@gVoOXi(m0vih}Zbpbr@hx!4oFhdw9(;;r?+Z0_DlJ_pBH@ ziNc*PZWI?~o{#f~o{T`(-!b_zcX%9C;QrEL<&5CK?nM z%=K-0CeJmqaT<^~#rgJ+S|z%1s=oF;RA%}pNpS;A~e z+B6Yrip@jM#5)xv*t@B{lrk=mfl%Hs#8*A`8}&XlF!&0;s_8 z7puITG z6w&&X^(CS@p&Db3nA6Lkk6o7-SVIB`hrl%NIDPc{0L{|W<5D0p3C#wpKxP@rkppB~mN!kR#gzTnAKHl3 zn#8P`2?s{v?48jWFgXhMby0}QR@YU#H2;@F4o;~&bQ6FJlWYHc8j;V02hPEKky`D z?W54?1E!W`RgPH8oG?x_7iA~r2fDXS!QN5n&}YAH?47$PC%8q)=FHaDoF_(5VTON0 zVMQi^z)-u2lo_%P@2;}Lwjz5Km*X&wR&2d!jo=JE4+m%0|i7I`YHdXWM2H}P-C0jPi>J>|Eg(XG_C ztWo}JIn>X-)Nlav3&KamF5gDYj~Qq}aX)kY1qvRj7sgT{bX}^H_Aa+uwI}7gP%>0B z#2s`QkR4BcLOx9SIP^)X;?S8Jzn*ZeA4ziMoq8(2BB(S)GKkhxW(m1e>I7&CnY!Gw zN{Pgeu5ozO^yiDZ0kikolOumpIEu+nkZoX{PH4+bS=Eddm$iLQhRL1RNP$levsnt9 zTBvyCx_zj)897@iseDCd$L$64HH-?O8NaBIi1;_#VE;D|&N)xdvPDzPU)y z|3w@WOA5kIbn@?4sR{bjC}B4{kZFhH8q1JDavEXtd_I^g0l-pUuCyl~oJC~=qBFeo zrQD~&UJOg6F`>0wG2p~PHVC2xRCY<#|TcAlsGtdkl4sfjm3Mf@{lqB(& zZ!y`*(~@0N0A>|^511=#O7?@YCSHqUrBeDgD&FCRSe?d@i_Gz&bvvPPxN-5>bxFVq zPjMy<4Ro2i@AFb2UFK(&5vFS%!nZ%Y8>2sO5jO)XLiy5HN_Ipb9i|^MgO52wfW&80 z`3~=x0QVo)bK33^&rW>5x)?i_;tU~CI}v$C6zuVEj@iM_x@1= zZfoA|akzZ0Uy>k`i%M*yCayKLzDzA=DTLM!0Mr(+sUZ|gn0JgIyYl&To-os z+>{;49oEqAmlvZnpj_Cgg@ZsgZwv=v%r8i24^plm#2<783OGQbS;WXZiTr|~ zHzZ!PWdefPJ#)Q=Uoqr`lJ{Wd=5Z8)r7HO(Rm~EKL^c=3DbWt>{*AkF$)&tzwXUO{ zfFC;3$B{MPEuP`$>Xlwr_bzE)DLWiaO2VPz{}99pHnSl;VpP*ww#6NuYdvySa`&ZA z8NRoJ(CyLKtQ_iiBKgOrIEzs^yGiPR9_Kr1NrSg!whGT-QI=T?vojTHh$oWfV3ROK z)LjBejsA$Z%Jdbh!|ZRPTyV*FadgEw0lxN=Gsk)>R8OuyEQ{dij4tC?Lfsg|M-z1Jo~|vsHnvM{t(EGK8Ul3j8M-ytQ`@<5jyIp z{}A2|b4SWDhPsP=QhR=? zzq;64UlHg|WlNyOU7NPy$<_7(tHGV$`d3=hD;f5#AxsQF7mm69i}=dI&#8Kqt8$x^Y+kk)3ZE3%;@1Vc( z7&!k;^;Y$7=c|-il&^%Xe`43toW4#}S*p)=$YLlTWDdJ4Um7bRMtMofkp6>Y#kE~~ zl43ul1bG|~f8Db`bq%$ftYvUk)yt6HiwF-%sS8KTnL&lN?Bc>Nq;?6hIosiD)Klv()WFMpFc4?kO6;v~~O{ zG?3CZ+t@XHm8D@G)(ivB;7`E>p3z^9eq>mzvmX=A5P^b>D43EP-2D^A9OL%Uy@nKG z24Xu<_wX6bCl|=avhXs{znG%)z#trptA?x&0vySk0z}e_IK`GLDJNOmbksyUF&n6A zsY_OYXQ`s)nia}8%TkkfSV~4Mgq()otgP;m=vCtYAuy?FO`@uR5y&?0XZbi9>5DL zxo#X_UZHhBoR-w?b3&TFi$%ZAACPj_*hTH6y1_crHFFQ#k?AD8VQX-^t{(7$y<+z6 zX-Bw;8yJ52wW|aiht1h>3Zt);+m$la+p!9>yRI5=<9YIWBd7)34I$89{8{Wvr^r;{ zy7)7unwE!U=`$24Wo9cnOTj3bp{cStn%l(4x%4u?Vq-i2TTVJeRAQPW@*p63Ycg5i zC$yG0Vam<4$N_G!GdwCeREB=&Z`@%t6Zh~^_5(pXGExr0#$RbPqb-~3c2&v6vYD{f z*q~O%RJN7u0nYr({Mq?7yQ1-S-#a*35dVlavzFWAPvn#%Miwhf^p5VwP${s4_9@^r-nlVfh)evOAtR7CycHNl4NB6>qoSYu2HE)Z0ZDV=p(-08Tz<5PfpIO_rl z|7~Lt-PrDy3~!IjN!x7i;mF2sYO5(lGXov>tTyZe%;U@$c#((^~UP4gJ6nkh$sFX`9 zXxJ2aJV<=NxFjee3a^Egn3Yd=PU}x^Zm4SIUzmt61$0!Gdn^mbER<=D+j=LG4m*%f z3cHi)MSN)a%+i-x2~JbL+xDLiZlHT0OkqY03jGu!JyNe3Mq{_)B-eC+=eu*9!gMpJ zy))>Nq_S8|Fa}dkBGM!JrNnqSPQ-rzFcYg%X$+Bo%UCB>=D;mSP8a@Jte{ViPPm`df(aKssH>o}%BlPPpW{STo5rxVZ~_;pt~|8sryTgp!Y zNNIqQjnx;8=*t%NTguU94ns?eRaA@B?xKm+Cb4T(HAq0Yc22Q7andA8ZC9sN`&I<3 zm%mV7N&Xu0sbFBWqf|(BShsB2j|wkb-Up~kH_BlBQ%kF+Uak8#nSAq^EASLbI2AW6 z3%mD{i>(H;^Pub4pF*(WZ(y;CnSM>_I?y8R60Lu4V({W4U+}LVVWZhHm~f?`=B~!8 zjS2_7N0VUOuR%8VJwNW^d#~Qu*CA_0>3gXeOKw&o&1RaNb5{2X(qnJ4na)L9HXFe3 zBKP2Tgqlmg2_@T`cKgF=wZhe;y{_t=y*%4;tE-Oa5>cjvs?2NnW$caeGhgLHm37ks zvN~qs;f@c)Jfc_Ebw)4?5v9qGa8?V+ z0j`TL*A2HP-F}%uR381654j|aU10#*bOm{zJ!Syrl_s-ETjY(Yk~*07NTekz)s}pY zlY94l@KX^07u>?rXz%_yppbf0s-smI<5p^$Y4zK?g!cD-l#A#QggLD7EdPVs5~%$j zB~ZS9{pNKQL#^Y^sru9l@=3Le#Mk8dVXM6cjamiy?_4P@F>-nTh}TSteetM6MRriQ zGQ0hlbSk%Db##gS-j^@|I;A1czgn*#z4>>ab$;=~9T(=t>WHCW|FS_|t_@ya5ygwC z4Zw|AOWZ0KqYVeKMFc81PgO9>X$S-nVI?y}jQndwGk+1$RQ@8gtNxA9{&!X_r56kg zGo{uBiUt5uU%!i93T57h$Zsfo4`ft8A_g7{?hPtwV9{!5)oX=*(9oy5|1 z9c+`WugV@xo^_tDzm8M;K`i+g*X|YssUVmjpaRdk=Em?C3eT|BVAGOMB!JJ+bGO#+ z%Sp3E$G`y;F0!9HNukR1pCrtGRR>`^aPz$401CW#2#}!28hmSe{&b1{ zP+nC1*Ll>zwu&(;;Ujnus!@q5@!qnhdZPU08OzGV-dZ-E{RNe}(0|5p=1%enArufq>54RUXlG{|FC1KVWo+S*d&upxI zfY5HgaGu?hM+-gZ5B2oe82{x@cD9AsVtf)0#rruE!IP5+3^Ytz1HEu$$J%KW0lJY0y8Dn2pays!f=YzPTY`}QNMW3`p(%84gA%Ls0N|He!odNKxjoiBM8#n{<@=_@4puLhH^lC-KcQhH*h; zjWSrCFGXAHcJJtGzLO_D%+tgg@pbSIoW5s1-LGDcn{HRfb~!%Rh<>lv%LHt@+RUDS z8xT9){Yj}#k2Llgup6qWyIHGF51;QZ_Wm)OP}})I?14}_@j+$ZHsh}^2+(#FV_m!Z zGsZjH`ZBI=54}2{eZSd3)}io2?S?$X1>xU~%DLYuL0?^eme>W40QpsEGjci%^dqgqQok&Azjq0-*;aKO*P;Wi@8((Fu zFQq!nUcZ!C)m2qpIetTnm2~UiN(L*Q(*mH#+%F+eGGm;Xa;vaXytSBMbqAqbEaDpn$d=6liQNz3U@96`J60YmU1g#0mA;2U-_xKE-jWG*XKVmqcL*rR|mojLyS{ z${74T+t`duB%5r-r(@YdbVaGij*?#GyS%xYMBBn>DSPGsl&xq6TszkL z8;$WvF|#>PLd=)y`ZN=454q}Ke3}eMV>NO@jF;<))=_5{NLZa(sP$WiqT2)3cno2) zwUGnm4#$RZ78Y94k{C5x8kYO9qMtEq&_)|LewTe7N%9qykrRlFw z=-j=!XQjqor(8y?q~2)&A-d^We%0S6)up0abSg@&6sl{=cg+ULOOJ7>J~PUc*Do>3 zIY)H&M=}Sco|b>LX#bp3q?wFrdmi6QKxQHd%47}bvNh~yMq{l}-oT!(DBA4iX-W`t z*rHG*b9@gcWp8J9Z2Q?vjNA7OLKtIrQgzdMZ;}B5)SW))krA&CfOa2Z&)5U?MSFT1=^-QDEMjlvOe(xvA;__E%b(su)S7sEN60ZeTBVQn{5OC|6!j8Z)pvjUaiSx&iK7|Eg}u%c2eZHrdR%?Z(~ z7A8rIF6SEPI6kFAey+!q>0M4qG+vl;_5SD1;$o9aQ@boAfEg!3zOESCRgI1n`r-(z zEL?Qdt*r~(MnILN4|k$6VMmVWJzqa19^qa>T0R)c# ztqG`E)*NsifDqV?GKixf3)zAa&&w4YI zBaVic3UtmQ!LCbKTZAtlzRrW3RF6>|nDOUGCh$LShdsOIaE<$w5)nExQEm_6;)FV`|pJ_tAElhqmP>7m96L*wT6!+`Mtw2n$-``GN>WkCSo9(QI@B z3x5a{K-a!eR0n5T_s`Q0a7-uB5Q+(`F`DHIjp!?00juf*_=Gn^$F}i{PJ5k9rw-!Q zCe96W!e@9r4R?wSU*w&2Xi;maUny~de#>FU;6$ErzTjP9tf~1Uo)r^q4l&jndFq!N ztzwLq@|g}_K;ea|OSK;xE@{)fL)MN*0lB&kfOKh@W$!4%p+~yW)V{qIC#2cKE4X~* zmGvgU9kYJabxp)YWwdW($uAf?riZ=Q2){u&zob!ArDT40147pb$CZ-zd4@XhsS7b! zo||e0IT*NhvYHn>aL+G(#(!2H90Smp7{1hVsUP>0_X^Sow4I_cR4_4oVOMwp zARG*RCL2Ij)CS^Bjy_xXs*=kzLCEBs-BtfcpNJtt6JSm_E%dO~O|NW_O&{i^P@2&e zghlsi5=$hi`H^&AnTzwi8G^4lxwJ-@`b-c~D~6N?ctE3D3=m2BK;2Yne6Fc4o2uCF zJmRLj)kEjqD*m4GmWO>UmR*GXOY({nAPv^j9Q*K!XbgI69B3j@Ep=>}-%^SE`!-

m=3Pt93H@~KKeZx%mM)K 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

^c8&NPre7=TrA|$S^cY0 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

H8|m(${PlwUimJ7q2UeJ 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

cd1w)y_MUHehLxem%UP7*H!E3+`yw2* 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

PSCZr=Nce?naSmX1F?q&p@A4X!bOs5o{SBpk3R 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 z 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

Inad&hu4XU|q|r!?3^o5^ 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

  • %CT51tLCblp!& zZ7NrrKLZ8+zQGU+T5%8VX5 zTzWV2^hU;klB6~BArbxsK_nq88iy3^ga0n;Oo|}!j%!$*aU3*-)MhuNx4fa!yLXgM z%H-wxR(6zbJwmbM)Ms0zx?jl%Xs`mYR_(<~)-D}0*ZZfqls*k0-qM*rP$V>pfEiDR zvA_$R$4X{Q9EMb75K7XhEU#6X6v`;?GgFQ1D#sh>1UQu=xtFcBicj`|o>Aeb-fvJ; zKj6R;u=Rhn_sbqa+GDRrZ$WDN?-$$s^28}Tx($kzp?OWZJv4c!2SA2yzCzmV{J`~O zx+#(i-oF1cSo!_`bH6-`ZJIZJyyhmrDZr4Rn8_fRd`-^VXM7SMI zrp^|+De(=m5fT3pOaiPB3q}x1s0^XqhOU-$RhAuFkI(x2y{d zi{Pz5b5@)XH>7@aW~3pgDad`n#EpShC*X7O@~H%P)qgL{Jlve@l$}28o51HVqfe@A zxg05JZZ~AL;RR@e>y{j3#PAy)jVtBW&Dg&_>AJhV@X%m0J!)C}$IbZ6OU#!2X zG+TI)u3mc{K|h{1JY^ccnZ~xyx{?ADUvkW0Ph=Fu7lwD+%0Ad#v=%MQU&TL&$&6$s zY~h{{d%c~A(VHC9=S)F`H3}3iyV>-}6C~Q?AkKhq;%u_RuE$Iy!Z6Qavzy;3Wvw(# zd<31YTIm8zt-d-ZpSbaEoq}thk$%=MRAW^J-DQ} zNO844&r7p*MGeXxwO*~@2J7us8xAD3NuPJz($1Bd>~mo#LEU%l zKu2aIS<3a=%UNesAD5a$m^}jx@3)d*6YEAx;b;ss33OCyF*W+id%JCS4yKOQ*%X@Z zIhz0|YD*n(ok1y3p#kbQiEv@I`F zcIZvWwSF;v50iJ0ty?1YDM|hgr8`r@fel^Od`tYTsKoXJvpR|_oy(W?mfCVs$)V!a z&Qs=0M|SRMpK9BLtc`M!)6(l|=?kZWx#;e11t#6`YrqMow^Y0;12yldhSLl!3N~Qz zGu~V!bXoS?XH{(*gZ4GiC@7V(_?gUJ6>;H-PDrA&qU}f6nDP-5L@`X3D6>4M^lYM+ zPRbSSQNKe6zBT{PXM7SXqt2%DbPKjaM3M$-Lzg%T%b=)fz(=C-mJqQY7^e>xu{EKB z%R;BY-7yPxN&NNG9D84fIJPQ!pBJD*NhL9WyDjX461FwYh|J*qVgVe*3cL}!U-;+& zgiuWxNr@F1@4l;mPu{;~I#}F2Qr)W9M+;RIxLJtGqB66BD=8v`x{)5nLZ~N*;tRl)5us|O z&6|~K@w>1R`c6BJW$0A}ch+Imo>T3_no)4b1!i&b2~jc@A&TVK)_T9C(Q-&Hl|{;7 z=9zO|@IxYJn4A@XXC56e5}%%phwVKn2*!x;2-65DWsL~Eg*4<4tZjcqP;LP0@>)&7`g_=6XMs;*6WEav<#TdH&)OY;KPmm4kiq8fj#ArBnyfmhrCaaP#}SmZi!okYFp=$G4krQXXzC0WvdfSgvpJ#sbBhNNhd5gkBi^;NQU zbC$4x?jSQ_!Rl0G2F4{7D!^XZ96<(}VqU~EC%qselGQjMb_!>0nE+5}8;VvbUBPAc zi(2VH*IE^Q5oQbIAt~KLVEDT+0sC27D4b!rMV@o?51f0P(eH_~$VI@>uV|Se_*e?Qf-ZOSL&PEUo4`yUQg(O~b^d)i;9~ zJ5$=7tn2wEnsq6o7t+`4WA0<0g9>2=RU}z5b z&M+q9w>?Jhv{I|FEI z@H_lZ59;Lg&kLmhq1qQ zeX8W4Na|G=?5%9$UqKs7qW0b7)I= zHKhxMb814h?0o4`7`*Miwad*e>lmCuGhFeK16GnJr;UG)PA_zuiaMO_D=SiJU)H8F z9vFG!XA+2!$im3Qn-PDlx8?XyX~Kj(sQ9;V(|Ft`bThNsZ`M9*^i7Sz0b^oY7e7X53`;*R!jScdzp9@vBn%{D;>Q#+mnB-R{sq*@{v~9##Fo_lcTqP{_fl(EHN}3H zY%8>M5;cjvjg8$cugrtl(t@PJdj5p8L3_kdp|^%9kENS7D>!gA4}lWm5W!hU{&cFQ zGm$0Q;>$4grQf2kOEYawrcLvwNILwT^VVhEEYgj{o&`$V+NrX5x+UzZw##%&%!-9+ zQXPP^MYNzETE7r9qlsC!=16HX@sY|MHegC^1VN)1Dlu4t{%@+in_ zE271uw)T6<87>VJ?h(7j$^98hh$fF)#&&aYIsR ztL#|d!*F#NW*GW}D_NJfJkfI2E+lbaevCEN*y-=h!uL;HRZOU3@u2p8IKgsYPY^Kf zV;4+3B-rTE=b_=3%}m3PfR()JGTDLFnhT^ka`gz`kP#F$4l}Ajfs&KP6u(+%sSlZf z>705h5o-sB3FX+nD1o4Pn@GDq%S$!;kDQ5=EHt2bKv+ylL`AEJ8e?>&t3kgc&pn|I zMy$E5R;p6klWv>4-P!7POEjyP6&b+ElP+P396q`-^w(Mpg_q|*+Uepz*~{drqA`ma zj?UgBt4^fIOs*l3AlsKYr7E^oHohHF(mmpN#kJY44hm8hrhDuI0uVWRQ>frmeMm!0 z+qi99IQf%c=ncAyk)a#`Ly+WFy3lWj6m!Phz(-ZXDIElvkL#!n)Mr4JjRCm&tKQmb z#%6~WQPWn_n9e39rYX22((5lUdm*IduQ80AYgioPvOOBBE(;ujdk(Ee#}{(+*F(u1 z@qs*AI7`e)1S@SaPIATBlaqioLmj@&M^+f>_!RCz>fAJ;fMy#LZZ9`vfFpl*;Dwj1 zM67BMRa!OM4O~A=KT2oEAsE2w%g2*0xxQOy843|y>!|UviDBlqP(7OzI9y|4d0Xa)sn4@w zJ6&Q(^$cwxm<#gpnzKd@+EkA?s0_H@#!_wO{+0WKMN`O%m2cQz{0u^Yl|2lVtOg_x zNo{^L^H&0c*eei!J_-Q&#FJlj$;FHhziy^MB)+0p(5G0CCqRma$7<81r=g3X*%=4Z zPaADV0v~YVJ%SC+6AiDVQvXWW8K)>koS=lYDHH|o zH~xb?0M5fwY8CwnUi6((zMU#;7*8aFKkJQq!EjJxGAi=m!T}uQOBrL~5@{C<3_{Xs z-2eVZsLjH~jwY|0oJyaSA;$d+JVL3oka$d0!H0tehzV=yM9nKl>>9nT;1D#&VID%| zEmsUbzkr6a--78HCFqY|K=$t4J`LC)tmtWGXy!btegWOQ9Xs>bZl{EfSMstuS7#CM z8@$$0p3M-id?WwP>)n>!xG43{(^s;d2kysk`(ez*UO9`jvvixO1~>I7PM|m zA7bt|^^}(4k*_>fi;0Wk$&}KFC+XnxUkZhegM{wFVgueYS&nH$fc)j2K^oyH3m95O zeIT0r=3ct)E6yNWj=t6lu!*$C-#@D{6}A_)L`JLcf$*xk4nrkELmX&CAy`Y9Cpw z%LH^c$0fi}Hh%Cdl5NZVt__NI2FWuf3@(>mGO&heYgMpS&=xqK0$C;W-H|d)eLe!| zM5m~dSptj-v!4^&Op~-EzhI=dsrl_q674roPe~8X4!l~B4M%zI%xdhU;|3p2eNoZN zroJ(ldqnMaB+X_x82G93>u)+QJ|9U@RICcitX;=!NHXu0P} zV+YvpwWYCd4?F*~KRAe@_%-WJyaV5@Zby!oHIBT~msLC+LEd{?%G|(j^(4MIJe5ET zKrKum&-Xgx$h)8~1kXVTyQKrUQ--;%_+wd55vXfP?n+}>2Q`q#2RQ>n0Do!gGjX zTRY0kZ@L=uj8lejKBB_~voccm(OK~#`@xgg6>8{=;n3JRe4WlW#}7v}_|N$1Bml!= zidAfZ#V6En>M*bK?0`28DI-^TKe!@ARhP(oKJ|hgIxI-AY#SOUR;xoT&soqF6zD{2 z?1G`bdWBn#K=%Ts!sPcqrd@>kZQpGC!JYdW8{V)Lf27TjXvWAXat6V=S}a{`&LWMR zk);dDSF%M!7u=EazT(cZ1&ZQu?*P#SRKGoFj{nfn{Ak8uqWPjlUGS4d2swTNZ^MyPM{Wq$f&$q^EBT>uVbOw`BUFGZ< z4^e$-4S`be73}q$KKXHagtZbc9nrGf+}R`fC72@fMHqO3%0FulSauA@2276G`9^2Qu0%8xo@ZN(ke8F&mMP-_~`C{yIy=Vi(fi%i#k z_C!PahC1(R>WEECuG<-hTO$r0uYlNzyeu3S(P~6y;ugfGF_5eo7!*m}?2OhbK zpAK59>3Sbc$6S?@&v0~xEVwX0GXJxrD3$*b6V4AArUd_LVIgz&f<=g%_mB-by&6%R-+q*}TaGo}Tvo(3&TQA%*?(JMjFEnpTzH1S7K%dstK49| z2Rm{%q`u7W3L8*b@>Ldhtg>l5(VCf1nx2C^yX>gvJv$r#APFo}e$sTd|D9I-YgZNR z9yKLbrwvH;ACn5TG?E&XLWxSgV409gnOG#USW7*Tc?d(!C)EqB;2)gN=t2KY&cdxr z0$0rDee0;{Rdhp$PUXr}?V$vw68W+hE{=j8B<0&lqwU|!j!!!mJgh_iI-DNnn&cah zinx;wD<%Dx0ebx5(Vyq!4w98_I2lVRyC)oUm*waQ2KyXzC`ivy9&ali8RQPHWpVGn z@`Jv_V&D02=$}2Va=c_K9h*=dLpFZfa0OJ+F&N!@XuDk$l=6E7`@4aE6vY~MWNiQS zQy*RC4nHdW#;At5a&V*8a2nsZtm<3pylie;U+i-;2Y8Q>RA+W!-{puY>Po=9j?~?+ zs90~pis1Gtv5DY*NP%)YfT~eo*QvGr!cLrADb|TmBaKL%3-U9ihCPJFQE47fN_Xt2 z2mQ$}^N_F{Dv-j?R$IHxbRyMLa@$osr25ba-(@Wl=hERpVGKF5uaoZj(D6Yo@ho%_ zbc4v=1^kWu-VCvK-gpmYb`rd4NBQNKxl8a86V!=r8nCn1f7RiE1{lYSbu#;+HX`2bsTeUy_ik2V5Tq7PUJ?7|(F0NSWNV4l;|92?~4!9h>O4 zQOJ2*CmV=YHMa4$R-9sl1geLh^3DWi&7U*s0eYxbSa~mxER}1`z1lNM9{YIb=k>DH zA{n^DyT70=;I6Po7CSv3IjWHy)gMPQFTAsHpFyOO@ay;W21Wl#U$myomVRKUQ`F?k zzCyJp=H$!2V#*w%&tY<}-eBFokh5VLy4tx&+BFihuVrE1s*;KXRA>)rb_gdYSXri( z0168aXjPCq&b?o{K{xtYb)bxdZEbwjKNF(u)C)>Bl&}1N#?Qxju{mWsOB9`uKR;aw zuYWy_*xfr+-8<=7s6qQH>xJfb20$OGYZQNAvGUj{_$72mf}!o^m}<1Ot#v31wINuW zitd?l=#o++19xEG(AUGG7vMAC-tuDI0BY_?QQ6A^ts@XTN~nY1vxIj0*-0gMV$m}r zFAv*1s-!A7MvR)Oxzra)s;e?!K70OzZpRrNUUnWg{pL^HCNO%)Z_G3Jw6CW^Ch}7e z7|d^oaJY3$2{cNvjtTRI32RM}f4fn-5;iXRrIR~M{5LRi-j5wUSKdSyxzi40S%N>{ zlHAdMz+ne!Aq*tP%tZN@X(Q^m1^2zL=Go&l#vb2jLG;Hg>*$5mZ0%WKzvhw&Z9{8vz?Jm|@zY#y%Jf&}ftf7B&R}TW3<; zadZ;h!?OrdDBE;46M4u!$voTkVi!qmi#iD*7~6)X&$;W%mapJu{riVZ?-BKS5@X(l z>HV}eX7jE4DNge#%+Je&XFJf9z@E1{%xRW9^l7Ji9QJ8vz&!{%{o5_F-q9@ehK@!M zU+2GqNWIRZ0aAXD)Tdon`mLMYbw8Tn#vCVNL3kk{;oJ(6)vFHBKhraWBhc5C;b+ZMzcCQaye}lDdoKYPYqUy zMk^t6`ftPn5ZN^3!6!!23>FvK)fD;zr*^}qthKe2+UF<67^}*fs_q>B;U*O|Mwk$) z+WqVRa5A@N1f+C&V@Ez?Xd3rkGo*ufQZ_rPkg_{NKn&Ci^ME7rQUlNK7*FAmDu~78 z=zXjV7n;twR@Eismwt^h(5^=K&4 z?BcYY)NF;hP*mZ4d(Km~lE%*7_Fy=nl3Oyf=?zZwa~nG}`c8(KN~nwkD=lLC1XO6t zejQ{y0C!%d(%YQNRyXJ^ij@!^MOiBJe2rES2O*6R5iz;km9kTO5V_2J=VFeM?1;SX z?x-%$qC`H9Gu$qu=%$gdY~4t-(hXd&_%-iGYLqmwhyEHc&MQBVEjE)?ZCuq6mqsY| zm6+>t*EKz(_1vA@O@b=r2fF2J{mZkJrzbE7z%$vHCBj`gwYlYF^_Fv9Hlg7(Fz(@E ztG=eQ>=>ubY)j+mQq?9aDAzpHd?Y%gYQ4goG%~j=Q-#{z2s9kZh2%j-jF}pp8;&Wr|jxo^{2v%YthV zz*?#`Ay`IkN1@egHhcD=yGu)Jv+4*XB$?T@fCUgYI6)jD7O)KH^y2TBNnl=(yqwtR z(B@htNi$vb@v?Zfgvmq}mdzT}V^liGe8O6zM1s56(zD&m(~=zIuSTaxP6Zl@$KA}( zr1RI60B7*;0}5$f?VFUd3t>DN>HzKpgoXn(7x4SrzUTn8+`I!d?=^r_NJogxTe21v zQl`tgPNxN>Gqc9@W`|QQDhO-w6&Av&N@l}R=B-{JrgzO-#Y=t!@U-gJ=%H$87n!X{ zpfdgTG)*@eC@wQQA6R5k;8e{n<{Rd# zerNL3U;^Z&W0X@Q{5g12P|a(6N_fdvDkBxtAEj$%(P=O+D|pw&Bn8RoSq_qzP7ZFF z`bO5`k{`>r7DTn?NRhUN3f{Z`OepWs91AX-j~;zCSzpIvrS+AM?J8a^E%nl-YPH+9 zV9^)C&xT!@GfW{d@rj-Nb!cjeK5Tx^XJ7z&>eb)@*oNZ)pmffAWB=7@e1_}VBaOu zHPZ@%pPR((b5DsI9nhlzT`&O9)SoQ186lzE&fTHkfgFm()tidTF`pa+i%Cm(E=%xp zYv@(N;kk_`!Yr&YP?kA{XKALyiZ{B>G1qlu&dGA`7|}|{pw4C&e{lPEu^b||s{Ofj z$v2REB-c(+*QUiznlBf8U;+yIjxsFTP&QeF$tk+_=dOn%*ts(rabYZ#ZmyQbBsjUPD~Wf+YQDB%8HJ zEQy}brfK-FY&ybsvPucoNQbFMnS7+^=@9oLAGmK>D+gGH*3M0@VmK;^(UzUu#c`^U zA}cQK*mZ0hOnQU>&iX^dtT+edE;AXfToPqa%L-8LO>(UsvK;QD8%^yyYjFZf{k%dW z$4&6vS4%_g>n%NDPjjj*14q7A{Qmg(`APqbxfeU*n`5YKag{PMB0Cn?EO0}|5IX>X#WA~o0c-S=XD#=ZNYYZN zZgX$!iZ2LABu-HL>xF)+)B7%t0V zTngbi9Z}d6E?3E9$`YZZhst~#;5vA_y6jSWfyfiVj+HP|qNpSyrDA|;(wFL#A`=Y0 zsdSe37}qkuUCOLquDv@NlEedp9GUT#LUeOizRs4lB=8=(h7AQXc}+C582op{qU z>m&(%UQXP4inCYGQb0a9uX3H}ik!BAmDYjEFy@5}%%A$8IPdD`@4g+3a!NHc1H znhX&!bbKeHVkd*Nvyzau1vQ!W3+UG+qr*D{HtcifiX+;}*SII{PPA|GTl( zl1)AqzC+Jf7;UjyTf@)gb}B=oj=uR6*Tojv4ZZGew3tXO2@ju@M?Hi%4Hh zz9aMB0x3R`pOY4F$@1Ho3qEmrtZ|bE2=AVbrpXYAE3Z0vgC2~%6; z8pEda+R_8~vXk|_#xk}JeNTGCO7B%iAY05k^?@k0Mf0dGuhNw2;P94#h99ezKGN$U zCDA|9g{_P3s7EJf8E6)by^MpDj6vyLq#{s&Adk4=EzeRIdaZ0{nBpyROjt@@1k7lp zx}%=XAgSXR)3DmRLdeg|JXe72&oFuo7{--#tq+NIyHYhMumTZG*vBC+Pc0|Cw9{V4m{{tMktKK?)S z^CnFg1C_zlL{#i||9LNiK3N!5){O`BKMZWVU;>$aC?XU)y1(S_T9YZK)70MXUS zxSx!#r4jX!}Pie##CMsQ%-?AWFNh zLTC-9#~i}Arl(Y>FnG3FdCzkJ&h*JYLs0!^W2!Gf$(+S`ahE5!EZ_OqRS&sP`_r`+ z*Gq0LpXJ_XACfn5-`8&Z?;w1NWx&#E&&f><7f;Oq4A;lZU=QZYUwlB5NB>~to6p$b zIN6tO3IEbAx!=cce&$)mUN$Xz$OXvCP`O#goCi2GOY*`XdG$@IUh4E=N zENPn|otdGBQtWlawvPJF0Dc71{q=@G1#6boEIHD4Y~%LC3AUr=(SO_Q zROwbSjzMrHC|%Lk1!=UlvwnEDt|VUZvljyC@o!`~8hqjJFYAD{%9Q8=E4h*aZY`9w z3zK%&r~N$<`(zU87o=3*$vT_BpP)o7i3gJXOdTb&+#ddd%sq1Eu~Ep?WJ^y;dO|&_PtZM+3KxUJuyl) zv3V%pX+B1)Jilwac}oAR)8gqp(~W5} z2#XsFTBJFXGdiu7$ajU`b+`=u>6)C8vu7oH%AXJ1vH%wvIRw&7X6QLu6%v{c+N*|I zOkT8Re@&xVZ&drRb>vtNT9ddRo@YKs*_2i*NETYP7`b;42%*_-tV30B^va;rZMLj; zGbupSNTD?qt-X(0-y?lY!1I;nR=RGAMp~+HA)bk{rBDbYS>|t9@~<}P`G-aN`Lt*G zWKx=qK1QXrklXh!;|msCYH5;Eq(|1{DkGd+|0XG!(glz*%8ki>zmzY&}&aA{qv!`K06+ z<+V2vb*j+}9TUIJm96*J#jgP^CqcX#ee^Y0f&pXO=wq(ZEc3&5p^iC@M$vr=)w0`X z-Cq|m8)%!v8TD;Ykja@Z<167EQkhT)bi3*X{AN2! zI^ca?-E9g3&+s;*J^C^JPc)ejjeWasNl$O8K0RI=@2o!Ne-Z%Sh*SV1QT1&3sP_wP zj-0%%RNAylj2u3{>H)Rvi^KKM-4H+4oEG%zsU`ELe&1iH5WA<9xPi|=5qN(xtX)I# zZ7aBS0AW#)4A_RqL5w4Q?Xx3}Se-Z>ZP=k~0A>mr8Ar)#t>2=+606H+Kj5H&RKuOO zMewikJVFTm zXScK=IApfxotl`FWec=)}NA*4w-xK6mAWo)ssCbTz_a4Jj$IxKVK z=iK>3`CpxNV&wt*WY?jys$zPny|UC16l4WOBlKLnr4+gAeWEgo_~4pMyg(s>Cco9{O#+E!+co9 zDZUbJ{ZSfm!I;~vono(^61z;VllS*cYj#jPpH$fcT|=o@YM1 zEw98+;NPL%5i@2&Z$#(d_6YC>mTh`#%<@~oA6*jYrj`(WAl}AL)pBa{^o4M*n)CQG z94HI*RA##taCsJv@z$YOx-Q1K`?@nxqJ*v;lurOz_uQU(dHR8S?w2@*`&UF)^NPMoTILG1< z9CTqAI2GfQ+E>B5m_Z|(Cg79abrcHDh)olmuE5~-e?km}4f3WUT$V-yTp~jpp7u35 zB@F?pik#@T!9LUfvi=oYMNM)_U0@gd4cp#S!J%1dD&G`JH>leeL6VjnFfNo%Tv!Wr z9w~inI@ObzP1uH9_%$^@B*d})T+|eGK6BZIe(Hes@D$6_&;hV>aDS(|l2DuVEg+CQEe1_7#h z(>gYnP4>64UbD`X8rw!VexawQ9i98n3Zd7=oPNC=p4nd?AfL~nE7ACj(E22s(jL(C z`y(G8$ozfQFO*yT^o74_B!GJ2Nj+4P_gFQ04+d%C9xqYbF4P76&edzJU zv>f2!B69aMm51v0-O&FXkg+f( z>4zv;-I1|&LZKaQPcLi@HNKKkwiq{RWRqz|9n5|hv~m(eSR($zDR ze4*L*&i;rZ-DMiq%IXx+ghi!EvEO%|Kp?aG+i_99GvA6G?K$;A%8q_-cn5Gb*%Z@+ zkEM?b752oYWPPgM?+&w66qV@r-%lpl~k3_ z*KCoZ{N9tDI>H(OoQ@Gr6F!46pJ%T{AG$K5w{SmN<1eiVGKrhFQ5CaFpx~t}!R>;& z1$+9HMD8y`h})GQ^QlD*sT3kdKRGjgnmK-DQ?mm8$(U_ZscmVN#sO9#r7)75(8^8V zkH&RaMtNwGUM-TrG-wxB2v{ZV(hk?6mF(Bez$Uc=(jI!~ob<+9+t;fRD<~g@ozo_A}kY;fUgn3$=~C zJtyImM!RW4c!>A8v}+9#c{}d5sx6Kil^MyRjp7(x$m_6G4na6VDJS1z=y+f7TgTMB zAja}oHEjN2>oDq=ozQL;`fj}tt1?Qv@AY?K1puv)wrPc*lEooQKC@$-N)z?EYY8qjVthSMGyGYk( ztZp*A9cAPUkn1<`xmcNh0Qo`ihsErBT*U{)F*M#eP4j;H9d9^awRHeK-q){z`2Nrt z!p0h73~Aw{+=|f14-t*j!jlHVK_LNnVhE@S$xe!cT649L4lu}6*V%3L9kFDuDx%GF ztGqm)@q~q%Z0A+aHtuC&4fD6@N*Hub(W{-OqXbMPo~$wYt-4DYX{h0vKIY(p@asK-L$6JWGg;R-0I z*YDJpW)|x4z_M)b8vWEaEza*?_ea_@vzKABo)mvWIw&W!!2T5$U3`YsrL)IQl(i$5 zH#e|?^nebjD5?5QOPvCuJqG~Dzx`DmZ~kBnQLyS33RcTZmjq@VXM?F4?mj*9>9LE* z=TOzFaacz7(Z+bn(O%eob+?6CWL(_|HOtm+)_JTV9{-NnRV+W>c0@jkk?jDnPo=&G zeKS@FlxC3=)b)lzUMxWAv00idziuU;vM%czMv$74G@Cf@bFtyF5zhrUZ1XtiKr@l< zrO32PRc}L8IbX*Swrf?jcq4H#>Cy{e(7tzir5;bZ4HebTJ5?MNlcq{{HOWTGphy_< z1w1pP34pt!w8jqo!rQU?HS|j=wLn?VC5UUF%SRE2@v#Ek6|Dwz;L_ctWG;ur&iosU zHDGsF)R>v3Nq1f?SLz;++jrW+M~d(jt3Mqn$h>%L>3812qw6aN!DKC#KnKRp5Z!(_ z+h><;y&h$bJB#AP4HVJ5+;bd1_>Tc-~&}a1~0=fd@9o5+0)@J z6=Szy9urR@RMH$!h%z)_C+cTGOw%A7h`^49>EIDCl*kLN*{FaYef1-6jqx!VRtb}&nX#F@dCc$AOAywyczyfBNV`V zUPvu+VRr<{ELgc;5ZyEfDYc&@9a1z(7J*mVNzz}6&%_Jd9qGX@(Epu>X@9Du1O0e| zX8+&N%Kt``{@fL~DSWda;3z(&zC<&A@6CwSbu?ZZ~sgWr;G${+>KuJkT*TFp1SD=5KKJ8Lh%0um$l?3C2MXyVWh z-WuWX$L^y$c9IPHQDW0a-qaX*h#sjMzqrEo%*Lgz@$ObWgh74FgKEsH0Yr z%{BB)rMC?=LztB&+NyW0R(R4Ipkr*&>g_DO*4#QTCKTANVN68>b}Ow5ZBU|P(h}6B zs5wJIZm}!MHK-3dH$xoH6nzCfI*=dYOnVO~Z@4|31sC z&&_>&}mO-Caf|}GXkn4vnA!CF^67>bq&easL4aJB-O)b0ygCYh0rqY1g%c2 z!}NvC|E{=eJRjbTkl}Wipt4j-Jkk>(V%EXHYLAp!Vs=O=ql7fDvn#Uv%FmiceLluf zUIJ#`!)1i31k@3{mwzhE3$Wn#+F{>^S()2uof_@NsWL>66};!}CIcd}Lz2ouB;KEy zr`_rp@Pkgw=9#*xU`+l&n(*yXrfF>HtAtiPWrplWCoL3llXN_0W^j?bq)RKQjGdOh zNwy{lSvsR9i}Y8ih;-O87RHe06?2_Qg%7hCuFsp=%{7ZTsbza{(hmkrGhSY>HCSxew|b6bK4}!1e&ON$m>%c% zQ7v}tUDg=kGssz3aLx3R*_Forj3nW4VV7qMIRDECMq1}3Bm``$pyc~gKV`^mwACUc zk>Y7wxFK|qSAA?{dS8Tesd#Or>KgH*rmS#OU=M)TRqV5Vu!U@&zVPZTGX}+;yzDB`C=6? zekcwMe}7sckpck7S==t2v|QUr#_B;_6br4e;*K;S{O2z)C&rPTNP~Ck5}vAE(Aq{G z>Lc3@s!y<PqeI^k(YF@KC!JBaRR2O>v-HKJ4(Azm)MllI7Nyo z>>PsHJX=jT$XO>A*c-o%rS%WWqN%T?l1sQ)m1oZ}5{k#Ab)J1)yjO%Z?nU$U*yrKa zI9c)XV_)+x>pPK)E3&~YUmfRhk?6)2**jsdt|VyPKL42O+PHhR84vdx748n)yN8qh zRiBROIRU_*>JD<#QorRL5pjE!aH-!-7`d%DI+1(E?qy$!rwn!MJf>co86t>|sL--R z--Z{$KVFYm`PISS?Q@67S(4AZV8XNP6634}5dbcPt<0#E?uQM!C^Z@m9dyJr^|)f5 zom`<6t@3QV;MZ`{=j`*-dLRL`AEK|3lZ1TfZ9nIKf|TT?DV>ns6RJvfG_$~jIV5Qb zEympt6{(6en`IUjY0TG3Ub7eS>bMiS>xJa6#mcZM&Kd+p#4d34Y9ojWI_2zyXKLvK zvqj0{uc{VH4*C5{?)fy$M(3J+*uB#UFc)|o(QAPtQX9ng z9Y9xMNL`RaJ7i%BgWBAK7M8qWqTu=%N(buAx(_0-L!Qr0m&>(sVR|A=RTxblgoYHk zSKAmFo<|)F(xGRHz=f2)*mK_l*uw;or{1BQK@&B~e$*DQ5SIpOO?CY$ozR+SR=&4; z?mLd_$Y2%M=4pUtv4cZ0=SAcMQ=~6#17N~f-vzBMVv{wBC$3}@dS`eu*doi?~Kq(R(a1z60SM^dTV;1=pG+ zLwz7sX-yg4Sbh=JB4kN67h+rhA3i_9Oza)<35{QB+!Q+Mj8C@1GFm;)=mQM|BD=WT zJayMRZGK*31mOFAtCQq>S}rmB0pMan*v;$pVGB#x_&#zzWDrHYf2E?OJw)_H%Yg}M zxpPT?W~#ApwvE%zorqKg4o?O}R$U|s;g6*c0t#OM@ud7civAY{6uAuS4%$@D7DyyQ z6ITAlYXQ{q=Z_Xe9LTi_WL&ZqL5=c}4k&?B%HYlJOqhsuKqnHk2@?+yU{;F&ox-^e zIIfO>NL+{*xdP0I^LN+}=|qDz&Yi-!2so}wP~-*p{CtkfL+h>o)Wyu8mfi`N8l2f# zgLB2TB1);x`9+~u3$>OA-Ekd94Pt{8xTUCH3%fQS2EH1mwGR6H$$wFg(Mh~li$7Fl zi1qu?6CKJ=zGhIhdBxfX(7SDn?vWD=$a25FuDwCv2?5;AeMz_R|374X1yG#bvTcI9 zyL)hV3GS}J-Q67~xCNI%g1fsr1Hs*0f(Hl=LEq$@_wGIac~s5ERIzszwY$Hz)qC~6 zfR^VwXY^P=eu;O7tSWbjW-s4liwh4$HVCO!9azkFyKHIW5RgXe|G|;0i+V(jBD)=e ze&jo<=FTTNRjG`x!!*^Zi}&FbX>VUDj&t6tIYNHgehw1QY`#Q9-Y!m-|93cp`o9;W z!FQrRKmbKo;pm7V|0^Y;{&J{^0}gc(!JmI4e?XXRFn?>RilRJ71TA2`M9QqUDme0! z(`kR|sUkX+F8ttq-o0+iNrm3rW`aG!8>4bCEa0oK+(zt{pic(RjrUDBPp;8#(hpB7 z2r9$j@j3B|Vgf|Su(Ol<9=(_n@VY_4Bd~~DvRUl0c^yzJw;qG2T*!mIg z6wvtVHB158%q0}PNAq9`6zxxf#5ejKe}K0Wjf`-w>6JP)2j8$Q(oQz&4(Q~Y*v0VE zm{u%I_ti3iGD4>+EbNX`aQ)fLHxfui9BXe3e3>`YPNvv)p+DcBFgqNxi}i)-XWf}G zLWHuG!&@@EcJg-{K(BqR29ulhc-n`96gu`qJwXhH45J;@L6RDkoz`nLe<;Ne&&Qh7 z={o=GO5nc__SX&Er~mI@|MjQ85AY9FvZyW&aOMvjI>CRRKjQDB%N%e@>Hzz%`RL#^ z)c-dlE!H$pCIrv*3*j;J(?Ot8jIxnQ7E=!?=ON{EVI^n#0iU&kW)d-S3Q4d;**Mx@ zcV3hK^8NE&vtqFXy_y~uYT^^alfYBL{nXkHcfY2Dxy`iyM*Gng$3xt^_stEEq3dc@^^)@2wUBr%?n>g)ia~It=29z%T&FpU z8FjtK*g39Mm)5Oy*UuAFC&+{4XpKc3NO%~}9qb7n5SU*tU}f*tTR$9N!U;IbJGcxm zYY)M`xTb;rb>8HiZbWq&WzqOlreEfSgV;Rx+I;tPSa_$_+@#B}Fwv|TKA-JObio~K zd%W(LjDn2bs=%#qRhs!P@YMn` zWZP^TEBJ<;k4esZ|r51(%# zxqQ6V=i*rpF)44`L+X|I#8ICo+t9C~O0~wpQoh4*xR>}Q7`QIwzjl-dt%lY5_I4b2e_~MR&q2&jLBhc=y&9}y>4l-c zDgU7UGjF_m%3kIL^WG^#1A9rqa|FO|GY!!DCp-?IT74YMkBB9d97?6Mfg3Nj1_6z* zFKVY-FM@DR12>v5P*6swg=v-2;d*9$R}RYX;|CObj~PKC^(&t zoOZXVjOWbX!aaeIbs^_O4Q**4kBymx{&3w*jl(qdRt{QA6;s~zKz!mY_ zLpGQ^TO8Ore`Ribdx0;l&#l~sCFPbL%9a68n6&fqu4AiK95t^p8AJ+OF~@UdLYIh! zV$3W?Z?TF=HUgrn#9Z)XAx)T%hLCt{Q@zy*39lNTbI? ze7@|M!-%)nTpOq|wWmvq6CA_WaNp=JK|$st9$%TOhY-)`L^=dia)Vw0i>5L-i!y-o z*X}-VEF}5T*T^Y+F#PqS@eoj;p2!nOVVVE)xkCW*^~f#A!*@r=_l^_0BBM!emLE0y zjObi}>KO8H%&2cH^&>`Z;)_fi)Zyq9rR+U+q9Nu1GLChtAeAxt(N|{=5?qmy(N(hyPksG~hYy&w2r<0BVl93}_BI#vb^tlNhAmyYoQ z6rI52n*mux9;a;%1aeRCShUon#o=ms33Kuv=+tyWgQL+(s~d}$BpFAj`6ei=lPQ;@3B3&Fiv6~jo9?-2d6 zF35U7VyTq59jySK83Bcmb+NlXx~d%71N6_Cc;;^hejscFNDHBo1~lA{XrYs zAVgv!t%LN9f_bD9gV8rVon0NK(iM#jbM&m0B5+IpWS`2<7t$CS7}w=emz%CZn<-l# z+Kecg+b3~`xTjnw1d|$KsH+DO?W@d7SwcpUm(7bIpP_7MSTuhVSlA$!+7gSxu*?m_ zJ-SP*r}9fl)*htx1xjE`U`RsCW4ZEyVM?0@+Zom14lTfcRpI_dP7pwFa76;FnTLk| z|3W7fGXvWO@SAWBmApHF|91@~K*K=&ubExPnM;Gvts{F>mB^PxM5iOw9ma>wHYwQ= zN0$z8$+}1}&Kz`)xjy|SECzt?`A}#^8gMDiw^byWcsMj={(E$Ev+-yv>+qi6{(v`nTz%32?km!wg?~Sj^K5q*9Y)`7gRhz0iQ<%dCTDf z6v%nCX-KP56?==jR(G&y{9hWL@fHC5Hfqo~TBma8Iq5c$xnUvmrV z$ro2Jxnxn_jer?Q9gY}oMyQXFRdduiK-s*g?-T#qZFA4lpjKwO=~uC#)Oj%;-UD&C zP#HUt78TEyy=&_)UqEPQO`RRK3A}h_-0MuGI(qR&rC|`79P%z+IMH-fR?9YdCAEwr z6(^HPj8`fCYmRjb<7h$^FVi%dvcmR;w@l|@KrvQ0Yn3s%WTBxsofSWpDMYU^$ZV#U2?zm4nB~f= zNhuKhxsx-DiZo1Zw6+QT;_8?~ zVDcdywBZxmW3Bs{3nX_iCO`;4#7HMU(Bc1rI5p*d%NBl>_mzSf(=?roNSq42lsLI< z?(QZrr2tFw9ClClx$(D*-O+k&FJ*hVB%*3{KyZ+MlhRIdC;~uHAy4>`k-CflQoBG6 zSpI_D&_IAU>KPi;;2YLQG@#(}M-+g86NV^O_;$WOoB`9&8pSPSHHGnfZ0wintMHUisAgHhIYX2) znbM$)NzeSZvL8h}Tvr36JMru}I@}mq32mb}8A3NXR#QEE!hnGHk2@dy@CEynV5kK- zcH7}QX}VZ3D z=KLc@+W>U6_*+eFu7XU&&%VbUYd7b!dVW+xv9F@fYL!0|RRRrA^)E=hp7njDkyfYG z_&({<8+j`pM6w{QrwV_!zEW*mGnavDw4$9@W-Ho6J7s01Z}1%u&M0E^I-TOp&SB~3 zU|TXsV8PTjKve{==)IS53Rx-L&HRcE=L~e1x-xr@j&BR&WT(?$G*@k|a}*=tLWIVQ z_MS9;FP%1TBwr7x@x3pdN4hBV&PXk*bt zW4qmVVT~Dr9jU+`AAwa}{~^8(BgL|w0EaY*-*TaOPsr4yHSttTrTeoMS;X+}ot?MX zkqv*ws7|zN%b_#3m=cMymCK$H<@Y%VB@^yX=m%%aJ6QF9fb9=5Qh-W9iwit)H@vcz z|1@%cg&n3Y& z9(Dzl6a*A3lcIHtbW%-AUT1@+<)|Xl+Y+x5`zx{VlF)a z*%~|Rr!Zi)AjHQ1U>tw{T{FWEzbL`F`THY~Ob#4P>5%-<2=XwAO}nVn#|C-3{ic)!qwXxK!586UC6 z?j2^h?x{#=E4uvp0);xq0gcYB;xUOPEk?e~vG%6=@T4dMLT=qq5FnDu-U2g}B}9M? zPg#hEd?Br9{|liBsbr7cJ;I9*9UP5xu#m+_Kf^H}3ve~fwJr)lkMp_nBhJ$57v61c zir&1-+lY-P$t{UgA-kzgTcdxz<4>|G4?^H`6o>drRYwJ!)C3EitVIig_J0ZJ{+_pz z1B;q3cpWArP*fWNA80%dhYe(Eg(Leo*s9bDXZg?7XWE#`mf+Qi;9`$91SyaV4iX;7 zuMUq6EHj3{`WV&PZ49CQ&n;DE^4tKZ4Ql*QK!DmL45L z=*nsLFxF^nzR6O`k##3vz#Z%@g<{cu%rrFDfhm@{9$_bcC45B>uqMsK<(Qvxe_Dum zD^r@T8T_GIrBIqWY&C5=?bYKnowH~Jcz+W3s-vyb6(@&~r8DT9o5kcv;(Wu$v%YcW z+&DgoyK&}nlaLkQhyrNi+dq_dByqXXL@+A7L>9k}v(Ve35I1tMjVJHfTTloW8)}$* zK2NSVxj0>f>Bg_%4c}UsH;9=I0lNHNv1Ob1;@)}4ucc_$oTWH?*EG*}9;GS3F61!m zoGk@%|MES#cEe@>9!G8K4$i+u}shrP~@OVniVe1NC#5x&y%vW8qqU|!mD{@_#FO5 ztjyE1zK~Tw;jVTLwJlTy!ix?|4S18=DqK!DGhP z*u0mdZauwAlLf3#HVbavy?8bKA+Z^|y;N6@mf15sh=I3=U7Tw38Dk{~!rpPOK962w zr8-6CHx7>~d;EYQ%tiS8CL(x+IeyrP`D(wNOZ><|Kx33RI-TmQq-aRPhG8mFs|L$# zM5Hb83dd_HkfRCc*lk;SSb*_0OC|7JUqE(ortFTAYzCaBRlB`b%?oeiQgXicQK4=4U!wUqqz3V4l)@)PPeYaZ1Vb>;t9ifUxun zuI||{IbM1UlxH&iA4MufNgb`$uMXGhk;pZr&VVrQnmye<4#G$U;G0NQ$lx@2cCrEg zyf^G}*c3&H@6TkDm(-j`HLTCrKa74y<;KnBqWmoN+jH94{NSW=hsk;G_OkLAit(H5 z=B2KoAcufD9X;ysWrqy?!7ziA^*co%6za++gyr7kZ@affZM3q>uj6vn_H z`9nr0nuvi#C))oU1O-hBg@HAe9~DiPB8y3=S|?&C=j}Bjj~GM>3jq*|3tVVrf!AP{ zYYFk!-lzB@FJ?XuE0;D5ftP_(zo7)p72jgBYvh8~DKp5s2(30Jv=H3ZBx z;{Kk^%>7AzvK#H%e<*+lSGt;4&p4OzqSC(keZw&>^5y(6bpJDjP5`tlxq@203rDcI zR8o~}Qs>7}--T)-r0p?SA%=SNJ56t%_=(xKD1ZsoRevYed(rrgc|!CN>Fpc~;*M7OP%F z)`-3^8!vEwrY_c3&V7|iEp?k zIj0$@h95%|+gZ7blh&3?^sd@kKOuNg2l#kMeaKj)zsIJk;K8-){lN22M|y=n@}dO| zc3_^!z&_yf_5LI)zT!mVyvc>`Ke(t`W%!3>|1O<6nwEXj- z;%3YhuI8V9Ix zG`1956&AbJTdyj)`v(9t7;p+h(h84mza1W@;kP14POe&HxU!pJ&#`5yj$4LZN|s1j zG0rEJV8*)nOhhSi>7OyA30B40sfu&l%s!@DmhR6saB^Iaoj2pt+}a!R&zk7e5%E=w zN2xKo^xygwWh_qm55%x8$*FHGMVtASV4W{#2Wx$+w(cVONUaXgX%uNQ;fD&PW39~~ zZ05I=@bU&6a_hE=SXGzu*i@&nx}}MX%yLKMo&oD|CkNOg8i3NIcis&+~wXd(-qz*gc@K*1&yvqphnk(fnk3o zy&>oq{Z?NV{g&zaI?^Znt^Q8>EwlU2pH;5NLDER1jpqPN|B5{h|C&9-aCIc3JqBze zvzTb7^njS(8vgO58s876tS$@tIv9lan}&~|c~d?!ax@LQ5gcdjuhCjTG`{PNxDydn zCQ~h_1bUe07cr~(|0Epkr1NEs~kUYiV6yet#XiL!&yPG&C>@`+vRB!mlHD9gSRaE&KM%M$c_TZ#_Rq z4nx{5%_32}`n=q9#D4p7>J5RA-K&(A*Rq{RUC9ujHXYALHFD6@UM;zl!_nkljxm>z z(JIqk)eypjd~pSXpBxb06m`jWLZNQ7TE9o2DeDnbHDO~B5^`Epa<1N7oVWyW_L-&a zbiJfCpIz6Lw|XkSw1Z##pnE~xsf;$ju4t5@q}XP$inEB)4yCi(d_m7L$zqt=sls8B z#?b))eBAjo-TRwcx@K=ae~S2*eccoLMahlVE_4Uk-2O9UOx?H7W`Am9Nqi$jjj8Kt zQ-jCuQ+q|eUuAwl_w@e3s)bpRilQCTtN~*^5}VrZK)5UYX?5x?yajTs}q;5f6#3hfE1Lcx{i z4jUlHl~kLaWvhku5$=|Q>u>6rl-EXF2C&7v^4Ie&g$(0=#i7|h+*58bwF+LTeTc#-Gh5W{HGztOJw(2 z0=wopFqp{+uH2abz3gB~fPj2h7$AK+I7^)Egu#K}8f!i2gz5YEKjVV_di7iOmVgNG z))8JXP)AK(w75q1`X(SNrBY)fhZdltODIWo{q74Kg68%e=%xB#6MerbF%N2W<1L^$R zP2Q8c0h!KMCg6kb1z)_=H0Oik0|x#h1wT@%_vL_NXTQohT=y@VRiJM4qXex{g_62W zJ}2m8thESNcVd(0%8(}$2vDP|oYjAwN}6(Gz{BU+srX6K z>J(~~MtJ;D{3PN?TPpkw_6BKIzVT~IF$<7_L*^C#Y_x=!-wmhAG<3>T_bY@HUEECf zt>kBNS<2G|+$CEUJl~h0TAe_l>VCW*=O0jLSZ!`l#?l)g`6$w97{!mjEvCOh{ZrmV z;w3(BU|Dm+0!72Xe%HAX9v^5B14{vLa9Y>E3Uu5Xv~fPk?5SFXF>^|=JV!DktlAt~ zYck+Ss$n$9-^6RXci2l>`{6RD`4w&NK==~A=c-)>5d*{8Uu=&DJn^*1$#y&eE$Q6v zH{Vgt&3Ct)U(dt2!tV$>6luxF!;#UmP;nJ)C40UY2tW8|B3dzIymnxIfQCW=(fS$^ ztSN(3B}wT&6tOvE-N{J<9mA4ZHL=9j3!>NQ521YR*wbB>_r1?+fL@I2oDj>nyjt2^ z1mT8U-y1H*Q!6+jyJW1W6nWV#mL0{`@wHE_}saAvGPmc7cxA*hTB$$4gM=I$&iGTNS2^egy$Wc6vS z;r1x@fSwx6*h8=g4Qd75rKwyBHGhiIY1QCiIL9g>$!vg5$xqEQU51KFF|P~Z{aw`t zp}XZ$lv_#AoLMw9fzqJ6d802pLkRQ~>6i-LRet)xFWP?KsyWpbx52szXg+9sCg!jl zSQ(Sgd%yILsvLkoygfT6WK8WbV41$uaSaLzInCElIf7Zeqyx1K1z4WMu*dwjX>2&hE=IAc~wg|CN2E! zxpHN5?B@EpOl1Q^X)Dt>pl4UpzOh8log{enQ!5R@q01MLDpHyD%q8>Ie8P_pQI9OI z7R&@V^@H}D8=!!t=SyE3Z&FlF|F6`@q6A*C7$tts&M&EwuiEZjp+5vg1m+hxce1t4U*{ z4kx^lY|I1Jz;|`wON{sM*OzH2a_>iRYqJ z*qVVs4S46GRSOa?0Q7}4&e^oL$ha2Hb{)aoxy)lmlvflBx3Z`s(n34WwkXOkDW9qD ziNip%G#%2WAUWQ_`Fa{Px^q9OxhqmM`C+MrrYAz*p0G5<%J|tExb7^8;vLPd@T2s8 z*}aZ1tX`O6nk)HJd)qHQ@w{yaxXV{;KhQ3j%L>23T8`2VvN@Ia?Nco;ynmfaW3FIbyE zg>*ESnXhvG=&5N`Uw<~z=W)X%SVN>qB$zgX%w?oN3j7&gS0c`9uPOr=-S3*8Mr#&( z=iX=TH?nZ!zcPNojmdcCefZw}!1rMO_H=Z#{ZYTqE3j#=C~23a{A4^alb)RX3l|BRwqw^n&26H14=TwlE5$<%jH+L%NeGd`B9mDm=Yt6Gp(shO{{#lP#89gQpsyj9*H2 zvI;lWe!_(_=bM%;9j^Y^Sh``^76M?6S)hZ><{B$|Rd1y6dwejO5}U%CaQV(w8uord zDHJX1RVe>}=c>7&rb=HlM9lhZK1B~1T~-PsXtW!h^?S_vf!Gp(vv|tB%9bHpc=Mcd zJ081TpBSLB1gFEzj+e`i9IF$UuuDtZuc`8*;u{zAv=y(a_wg`H8Wq;f~VBm*@73`sY!D;N3zEu*=1JDH01BD zzA5m-HHf{e1c}GVsvIVDqax=ztdnq~L zU3k90^4$NU9P0cKNsO`fIyW=J!q#_}=}VWbXdLPSUaCQ39?dz?<_wq1sMkyAd3(~R zfagK9bQ!E?Dj~&m#t#DA4jk*riKH4^Q^|3B|M<$*b&_4mJ<~Or8xe2e?C?(k#zu_1 zJ0QS)Qe}qLngNRns;iAz`Pp^c7OA0v#MWkw(uIR)@BN@1N7&%!_2M(Urw&8bvWMF_ zFO=iL1KPMmh`bYrC>6{gwVW)sD&L!&}dfb8OqBLYf^ex_= z>Da-Sa}voplJ8lYi{i1ytYmTV93-?xr4{{6=swdFzwXf^3G_yF$ooaTHoDVCNc26v zhk|O@8J14$9R1iSFDB;*?$yT~ZOBodA|^ia7Ms9;dR?`lCmjFVOvEOw3z0a^aycwu zyGRNT^IIudazN-?!JuEbKdPxr?%wlv_*)~T&GB-SwOe|TF1_50|r*zu9SO%|U7zz)Z5-AEi zG1Xf6F`3I(4`P~Aeg(LDrv1ADeB~2zgT|S|TD07a2InOsD%JeBAdwPU1pMsT@>^K&)xf#twzUtA z+&%t6@IrvizcXONraNReikM$1nW_=~cRx`rzZe3ph56A~PwQ(o{P1S=HIvS`%j6zIrn?TFg3v-Mqxu^nb-VDMk)q zEJ#&0Drvl{H8!#~F^MvzlXnE=A1ff#eFF&GG9}Ab`rI&7v~bp3#F^+kZW@hVGskXB z;$*Avu~Z09O_ z7wVq0Zniv3+kii_OA52f7r7g#b+?22PuH$fV?U`1)&N#;?gr$*{3s29>Cx(s`BCVf zFq5*M@|*>DSqlV^ycLWavQojr0FAXFuz>#3;M~v{9Rm9!d8;Qngz7){!C@|W0l@pl zAixSn@)sLL0gekn-~u2KX=|b%4OM3m7&ok|7FO_(B^DseOn49r<5oAx$G`SFHPp{( zKYk2MxkA~4EC@?J3;6!s?S3m`x%CM5LzcEIWz-)|BF6Lt4imPpVyra$q)zq}-*|FE z+8hq;=L%-L9}MhRpQ`&XXYn8W#Tu|Rf4=d>7oGU!71vrMn-l^DK)yxhn+`1m$k;Zm z-3KkjZ65IwM|n(Q-L=6m%hcG>N{Y~1D1XA}5!zUO57y6gRkU*lhPl>HuoC%;e|tKq z?HSA(u3%;oNc_NT!M*g>US-jJx8^p^9e)+k~Qd#hdeE73c( zFG4r{$K{G#lBu)vVXy3=T-J?G<7l_(21^?P{}w;S2ROJ7e=R@`lg4&5SoY!2Ko%$n z5@4Mo1VXYKh!}tvX84gxBE~tDL`~(Xi>lH*(M+!;Ir^=nMa_uzR2(1z&{)(tqH5E0 z@bUDr?8$Jl`q3Ns{s?c3D-DmdV*-)2B4jF-bMfm%fU zN*TU&>zhOFYKjXDQqj`gb7*!Y-*5{wNwVa$!)?nrssU&mg|qJgi-+qn542{REmR*W z;>{_0=2 z$mdh)%Ad$$0^5n1R!#UZ8#M0MTXs?O#8XFBHCxG)icfcBtRh-id}8fpSn)s{$kS{j zAVg%$5o?(}5fB6l7o({xsA8>}F7XFB==vS$14q3oynC`9_3P$PF|A50Zl;XmD8fUr z!-#s&!e3KJX zn`CRqM!iGH%R|)UfpF=w(5cHm{)74TujVLO{emX|YmN-`-=shsp!c`CW%wO31-b*= z*WF}ThO5Ec@~b$O<}OKwGqZ|2T<)6_h&{+A4S&O+FYuF*0;d!y_}(?`PyA>bMl?(e z&&EcMqwjwv!R)RRwcEmh5N(K`Vxje?$f6ixWPVoI)acM^w>j?8glrxK_5)3*Qs5X< z@VM_6vU1>19)^YiBgAwS9rFjX^CH}O!G~QMQinIJSmYo*V5McA#XP*cU~9-_dS5`5IO-8Xn0Mx}L=twSL** zaQ8l-Poou;LKn+EWRu=h3K9;oA47KE6i7Dn%d~Fm^_mR{ma&l^|mwtUUB z2^GzHzSDJ={n`XpKNjADD;(4kL((uHIV?vO+*vLM-E3Z_ls#<5ztX)~joo<2?0liH z{Z$gy5R2frD%0OmAF4=bH?pcN9cN-yvVz|*ta`?U5{j#{V!HAoPbjXiCK^7ES=`L0 z0SD^FFGJ!3tc67>jqzbO__5b(kX#j%|Gq099dhWvOVSv_(dM?h&$x);4Up%RoU=G} zpp;@sp!?DjoZ3r%pCQF&Ue3i4L|mm}hx&|PwTCpZn^g(EXy62+iw(t*7;)~4P_PWfA9vUudVpeC0N?3)oP^%TsT zB3cF>6@`-jMgi%Edn`i0>0})G->H0Lz`h80FjpJ!&)Iv?VWiX)S zGo5hPH-Q&0pie`7NOCyD34;h&*mHsgxr7$(K|;SKFKI}wRL&Qi!aR$J?pN7lT=+nr zj|SmG6Q8KX5qQRCq@|Z(9fb)wOyC{^OqrPalssB;i--Cmdpnl zV+Tp+C27i~T5X6tsp#R^lhf&@#anlGNz_`6#d|N2!Nh6WJ}WiGw5w=iX^eXw>NYi% z7b7Bt78wfa_R0{pEn8K_fU?qHN9ndZ$8_HXU0QTkb`e&QLTwtu@od&;)6ky)Ot(z$ z2Pm;~7lHt;7aMkN^e;1qjPpc16S{lDA1meK3DfFn&n)c<_3stBr5Y~sFJTxzLR-2r zV0wBdA4s^lZ)`NyH?~5@i{3<+c{1k%nu;uru4|~m^&qGz?|a4C z;~OUbtzY4Hfw&ycoBF3fHcL9A@TFyG)g$>~e&+o|#j>UA)+|GHE%-2)&K1Eo#GESE zR*r$ViOqs#zs;&Xp|^^J-_-NryuVxWb{M<6NWg za6(8q30-whd~Z|jb~xPdA4!+;QP7<&OPj2qRhiS0qRX<7!xmtu$*cmfy6YgjXnPR- zP!A=@%w5?LCnnuB%i;T@GxUl7^N^LIBU6;z@X@1~G=hxz51Z&a#Na|>ZR3)%9f}5x~srFBwsC$A*QqFA$It;5!bGRAr7MeBC zVa>=oi#qXsn}7lxVwDvrgaOS8@ckV1`H4IwrCSF1iJeh8gRE|TLHv`i3(Rza*LRpZ z#+V(3{&sgL0X2x0Md7+0vZM*XGs9pTNQSCrwvwX zx4Yg}tv!7BWxxymc(EkN7TE+DQ%$?D@k}Z~-w4_7)%8JU3NA5=f^SG6F@4_Rf{2}8cUpsf{y$v&d%fg z#K8hss?=(-^v&OhHz=!kJSf?tZk7APPwph2=(S9w8{YgJ>Sd^+6yN~4{`sJlvKe`p z*PbYKr?!LGD%;a@Ids^~w4_4#A%VgJVBhUpD!_Y%X5WyudJY)cecIl*tQ%?%3jIFX zP}}svd)#c%^=S>A5*UCPSw?MY@la~aeNL~06?)_***J>NXNiG9u3jBLgXJ|zhO$p+ zL2Uos=Gof_2^#iE7ZzY&?QSJqVt~nC*47pe%S^=B?tC>fq^VCSn8ol}Z1m|0O#Wxs z*v%5MAeyT3JjpH2W?+Ki0%Mcob_AzY5igZE_q3p>oegrf(JFT$hZrY7mh zL;Q^=lY!L^H*=Dc!M+P5YY9LfJ2+N=?FvWYKuZ)c)p1CoQEMN-y3ocZYT(fwB z{RlJV2zR@2b>b0guVQvkoIQ83JS_3)jq@9(Ld7Q%w1rF!_kht(PgVky;@Y#T@^iWT zbd`HWbAmZSH4i4}thPC~Qz*Xp`&Ficy*W+)iN(;Js%r+$LUAQ9k5Ad~>0&Y9uDm0y zv5T)fL^uU5+T_&_hxW7*f4KyFslq>ja@*pIpr9$A=lie_e+kYUD^d!RJ((GKJW~HC zz)8$VqRaxd%)9CLcM>;jQS!HRUJ12c(-n-4U#Dc+i#gRyv)mrH2-gMK*j1A7CUqaw z^MeSXUT!_zxUe~&-r1y|PE`1}{flKd{xXnc_U?{XFl{kEp0F@v=3AuFMp;fai zdn@N>xk!wNfF|Z|cg^NC{oD$X4Zn&5t>f6Z!~D?j z7Wv*RV^SYKz{#E%MrFw=Pe7Ns^pcSHX^<4=8;$)7>m1NTJ5nX)ztSmQ#fsA|u*lTF zy-?!+x^Mgy37!?)7-!%#&x$4F*comf7Tf>%5zoF4^aC=HhU9C?HCm7ey?w!Akr015 zm!Mjw>v!4LFPEWE7+@mvFwgAfvTU?Sw9)qEWiaDRfc^X9pF4rCLK~=cU$aOYq04q# zA{0@*@G>3Bi_TdWygX6y3F=AU!i#G(_6*n*LQig}3P~{-X;(eSy9nApf9}pvSb_|P2=CNC<&I(Gdu)te7 zEGCFAaTTfsjI3Tf6vq9k{S=Sg8b)5@()nKHH=yj3?&3jo@#zyqvubGt{}J^Y#cWf2 zUMZrkAN6KQ3yaz`$#2SK6GTOCBxMF84Cy+>>(^x_GwmK)V1yr17GQ&2T;p|M)=&49 zNuXsWZ}_-}067U32{?i{%HDv8+WR$t8Tg8^6Kn_f<#?MP);Lj6WzIsMIz>J(%D4Rx zAbuVRcbaRhmP@{pPJUS*Y-e6OEvYrmUUSAD8#~t-WYO@^q-KXXE%OckpH|g+0W#+S zY*jlx{=13>mM{Tu5*gfmf9i$#Q8f9@>);;-RbYzYB_3=T?_hwe6JU%Uyq*TQg$NIm z+-Cy?m{$RtM=wG`J%*hGSm>~m6bG`!C<$Wb1Zs(-KTwE0Gd!&ZAwx9=YbYVPa+tCG zxPc#pQ?52@Qesmj?oB(7w%n%Py)N26z1}=5JAJUIvPzJki$!OY*X|FvYRwEz8VJD} zL(0&K%_I2^ttXbH*MW&505t*vsoJl z61F%{7%x1CvUmR^uoK2g70c<4B7@kw=*5d!?L~(I!X+jh)wq6ofaZ)mbSo}6C1Au` z9PKtXj^fDg`%MO(tyK`|fs{RUOt0FFdZ*)?4adQrOA{x7V=m7rO?-lTP2yP~?h*z7 zSqDcxJh10{rP>h#G?!sZt4X(n`Um}sf+I0)?V`BBO(ZD?WB6#Kb@-|b`T)B|p}x>H zS8}K0=Aq(d)DIfP?NL@YQJfH0{BWbivSPrND^Z|=vepV+PdvmY{hx^4MC&FpQFy{^ z>xJ028KPVDjB5z-O5&G}g;(|}b^P7{PVHvz!56YUcPMs0&xnB*WHz<-o8MakoRttX z;y) zX{}Ff@VHb|8(2NfgZ)&LAgH*6WC?k}Y!hSMeoI_$;Th~rWF+2DMm+L>c91~(liv|a zVl_S)(iF&XA}@58pegGlh`sf^!X&b(Y%+Ah z4>h;?3J5_Mjz5*Qhl)NcT?kF}#{Vb>YeQPfq&3 zS1|CeR$n7acm_bRnw}k|D%u;(symnBy0)F2L-R?h{$Lori~^F32!Slc${g&r5_jo| zO|sDVAyrZ@mCsdiaAa+8Z+T>67MVsFwjis3n`KY?|KaK^!{S_)wT(N$o#5{7?(QDk z-Q6a^-C=NdcXxMpcMBE*!2*1<_Bq$NzO~^eJ^Xm5ySm=0r|!qU>1=F{c@ON$j5!iY zE7|*d3&rZM`B8ELf{={H`;-dQ!;Nj$fy&EP3WnuW>a1Wxc^K)!6@bN@sXg2i!>R(Sr6y^^{L?a3bM;cX(5-%>Ov)!i71Iz13MTs1B< zA?b;8%jHt({2YUVP5?OJLOXJC8KO(k{mC`D;=E7lK;_wCm0}jitx-#aZ|kRWv{%MJ zm5;m;v_%t}GWzbg->n{_@FA&o>1bSzIw&R;slcWiq2p#v0s)zSQ$4ah*a1IudhWi$ z+v&H9{5^RcE7yci#?bPgRm;8NaX9~1rqsw)-b-=zl0zUfBz08MpH-^Dk8%14T?;fNp!Tixyg}| zIQwS!!dx7Iv?tk6=C&9X-Ie0bPzVh0i}4=q-e`P!q7hm`?4ak_#tOjC&(6=!-{dFs z`LrPnwjJdrD#220?1hTFz7%D!K4nc@6(pXTo$H%~D$Wo^g+TDd1g4i)zgTc{mxs7jeBd|~CCJ|?9?VfT?3C_;#X<8WeO z7}}wj$JOPdK#kby#I#n%PrIT&4J%R zJ9I3%7P;J@bR@4bbK}Y1tVy+$SLkk7`%Ac~-5aaw*}Uw%oM4S23DWPJf5~Vv;58p_ z5MywexQAR^M5fQW6O375PxWa28Q;aJnlzecA?!_%7~LK%<;i0~1@|LABLl*llDc@gWSgc8HBOh=fm zK%@H+r?9}GZX+#MRXuNOtE5FWu3$mJghF-A&b_~`D~*lNvq`F!g^Mtq-VZMitaWz{ zE9t-lATnkH`?^?jAb%@n zjP4MiCZV{(2)(!Fh@DQ=p+|SYp<*|nYdZoYIG?tD~HnX_qUv)shWZdv;%U?E|6DC^e5?OdfDK*|14tcC*o+O!Ni%R{E zk|>m<)ONw%HpWD~Me>5WBDDP zHRS}JlP3@O0@-EBP6zF&n5z=CfcbLfu*q`Pt7ja=q263I)bPpdk;H(MG9f0u*^2*()x1alpJa_%jhg0BGmZn2G5`0w`6moLd<{ z7d;GnfM!`TdU@53S}N^yHGxz;JQl&%fh=^qOOZ|2QPNKGHK9NGDVhErA1`&Vsy$v; zPZ6H!?mc(SO7p#Rx~{IAsqwE|m*CuP%fu>^Opl?z)}Qvl*gkv_Gv+Ozc?^0jwXh;l zdRd~6;Ch>8(L&dhhtQE61c)Fyjxti7gb_ldi*+NO&DVz`aWDzTeh;U*%&ed#(kr)n zL@lmm+&&hiL@neR5$E^#N>r-G9lPmnyXJFV>?pOAc%%YBjcWbCl8pD25i46)sH`Xm zgKE|u#ZAjQy=!sk(ZuyIjD=A+p6b-Eopm~JIz6fgOAc}HcwI4P20(=E3B$j|r->51 z8a_yh60N+Li~r68_3k_~4AFSBD6?UBCrmd$3AT{3;s4 zv$dcprnnSAed6JiH3paXuLW!|MxjeFZoL3Z>W=*QR&?9n3pwjjXD+!9ZAGx8+lt%5 z3->qx8cOCxlHz=AQMtUJk9#K(n)b2JYt*1xWpqr)&!PuO1Vw`+1T_y`2Zo6xbeMOj zoyZUX$Mi;M*Zk}MhfMkBns>3_TBiibNJU_P1FA4zfzB#0h`^^F7<2#?Qi$lE04z}| z`=W^Q?=%uDiKq!GG|I~fG4r^VYRO6oM!#VF|4?G8SzJ^R@9$ar_zer zlEqR-Qaq6H_pq@DFcSjN1EXgcW*r#nre~yg*_Wlfrn7B|75t_m5P@6Yl95tL$7bP{ zu{wHjT(Y8VVDgCc%~ZsclA1MD9lteP-ISixW@gdK^SPm0+RnIl2oTd_HO#Oh zy{j%Mb$(IC&#>paZXXnQYo)2t|*Az5#qdA>A6xee8WXkR`A_$n=@PD{8GVGNh&Fk?QLe=&4++C zB-n>EvnFXiZ{BUOHu0p42vg@ONXaPRRx8Wn zen@d8Vy?i|2pXO?umxG0U+KMKDw!znOzI4pu}!lNx;&kTVAdmRQ_7`o^NLEug?H7o z-@DybP^nL!wg-}D4A|`wb*f74v41bX;w-1sl`case+bXkUgTJuC7Fr18B zoaQ@;&9MZi#6mh>mTM$*uoi~-ES)RLJD%((mBTf)7sLC2-?P#n9%vKt2D1=*3A#TZ zzEh-OoJ9Nk6PBh%^EVB_bdf+j*!6w+xVW8a*nK&nwdxm_GN@Z~Dk0|NH%zqi{9%4H z5ge=~#V(?UMW-~!q4TkC=Z2+bq4wjQ7(Yo6d;&_CKcN1B4>^L9v@wxD&iq?WA>t#+yj3MexwTgkpGfK#hih@|Gp8ATzv+vm#_c5z=aELmHaZNVECQrg=4K3B$;|qKSfL z9pM-<6IGmjc;MfH-PWyT&19|`?wv3LGFS_Eqn_+tA^ZBKzE2-dryv(;b2$@KSbot zeW{2Rz?={-)2umDkMZtGtBHoi(B)g8adVL1(1i{J#z!gEvcjAr?3PyjEf~D ze8aTJ&rMvwE;Bc1FxQw@*)GS|nBzTAr_#Lb$~blf=;{YC5w&If5#!iaoil5)bMG13 zX%R| zk4(A6Lqe@!h{;YzbX)iKgT8yoj@pmiaRr>VjoKt*7TbWWSVl%b?NrMkqLD~ORE zgH^*5Mr?a0dc|bu{Mz2HycLi*hfGCVVF%SfO0u>cXQk(LHSfSy`{;^WxGz3Ump`K#wPM_|w93%MvNsD~ zgRI5^Y!7)629sXG>%VeC(y;vQRStP?h5!AXZ&!zp*GgQ9ncE(p`jv(6$F+5*Jne`X zxW{<;n4{;D;FHVxPt&;T1ftH}I2RcTpz@2@D+X4eW&nO$*suT(TC(5gw;w)28t_7P z@YALUXZ?{UHc;rt&MED{pUF+wkVfcAig#yi%b1?D~4r~1MP64Ra01>e4z z{xRRETHZs@9?J&Y+DabFJvRHqUSfNmiXC4(*(bt;Fzf3fuW({@3Ly3&e$J(|4~x05 zUzbWz(k?C%L)9i#<_FfUk!0B117XwyUH!8MgKOTAc+DYIO$l7C94cOret%hrM`8>! z;DF7`w)tC3f88OwEd+z4dy?BvbXPCRa`dXx@P)+qr9|a|qFkx6={JwT>iPtwI%J3) z3J{<|^VL3a_RpsXb?n?;%O9X2j{F9DU4eat6Ue^$$lT*kpeaI8i}v{67&K0PR%goO zbuBG5aZ~0ZB>HL|S$)zx=v6REc^7I7xg+SpZv_!m66z$AWN0A*r4=$!XY@L!6{rPk zh59>@mz%X90B@oWX|x?WW7PsC_Q!ugLxd&q&)h+$9|`h*zzq4i@QA>D0(b-PcpkT^ww#M?v%5^ z4cm9pK=t*(t{K#s(^Xv4CZ!B_NJ*35Bd=PKYg(~Ml8oWeaW*=3Qi|hVeXOUNP$s2O zVFtClh1Fe;wks7rzDJe2sDDF=Y%NzeD)9{m+{^H!96rc2H~?eZF`Xw^t(DkAiT1V6 z%htG^7VBBBY8SNJg82TWOZzz&T8(e+o{lJ2?&U!5iTI9JK{2*$MnqtUG?&9~*> z8UDQk4)F&rtvh;CargC7lKB(s$ctMIOC7>^uT#CGKImel#8o^@c+aF1_~OJ)n@fFc zEZ6mBIOpC7Ljd&8zj^GX3b+@zzKLqZ#!fs)+?_GIRB9Dem}c`0Pn(ORV}_@5Ker#X z+6}&_^B82b8@XMs5Pc29$hm7?v6R%Y2s3kpZR@y6t0vK=shFL)oTAI&)5>pw3;!_5 z!g3Tk!sKc>T(+a?qds1Bf$p04cBkKDH9A8w1#TiybB6# zyO*9e^-TQ8bsnD^F$--hxtxhaBk=llS@@mDi8B| z@6>O&Bo2TYOQjJzAu({8-G>siQA7(ImaR=nO)554kRLD)!9zCr*FqaIiY1~my8{{U09Vfh44Y>?lI_#_|^&tD*wP_B9kXiN*VLX(?e3j7$|v;egOP5NFH;Zpk{tKR!IH}{=Ovxa{yQh zk?-2iz!q2vvSLwkKo?rr@T8O3C3&}8YxU6oGeO_{o3(OD@&1`>alj+6wEm^EI#dzk zPt>LIQs_$%TcQ?tA*M09ws;MSqd1~oUDC%3PR(bG-Q?@zY;+QQ($8Wi_YN8~g^Go~ zeHO+QYFR)6+l7ii#i`<0?rW(F;>3T8VTw)OqqY90PDV-wYMTAu0S;jTaX(P-;o*Sa zEkV?J5&}@!cYO;BoTBMwOYGl4;MB{G=n3>c+M$5^;~;e=Dl|9-&>0P$Iw=nqA`!I- zKX32f9|$VEw_pVomM@@ztQg=#$)D3-fb-a(fKj8d^^XQvFe@u(8I{Iivp5CDniSYt z%IG92gGv&GNu#EU+RZzQ(CTVGDFg>;iRYo6LRem=^caE3c&-0jA zxGxa4K0X*}vAyH_w{}LLVuw9#d$K6HmxYR&ik6Crj&L(^#t-MKGU{3ruWy4W?K3@q zwcLYF5RWR8E7W7^F5rf~oUNMZ(O~UYknFnr9Pi6`f-i|Znt*u(aMB@6V&}p&%wB%| zuVmla!7hD~*}|;p6f1I-gSXgd(Tv2M;WN?-_Aj5f^UFXTY2xiQ*dV}so?F)*l6T$B zgFd-+W?<|A0`V$}pN%@UcmZgp1X2q?-&aBwBoLMwfeFg^8K#^Agk(D?{_dH0o95{sj*jQkzjib3zxinBLr8T~5I5g~_^L0)#7@i!yED|6cDlJ@ftG*JssWy?{F4^I&@^ zfm060b;1GqjlyR+gp-VlL2SY;%<|31TO~wJ80UrJq9hvjLSYwNm&%L2%-Co=@_?=e zuTkU+yqpe&GGekbvb^~c2Wtn~z;1*{MKfc91?G*CGKoCvrt2G0)KSKs~tyM44W zLWv#Mo>b<4?;%kgwnH0$Lt>XCj1DkbEP+}oruz0k{8Gsd$1e1I9xOch%U+KTPLtSw z{3iG6rMKy9%k9rwJhKt_w3%Xf9Nt&%#KlCZQViW`&q9p0<8PK2-T02M+lXH8`Gdyx-nnoL!0-Z+y+?rMbG=s0>kkeT>}g2p zZg!)4Hc7W)dX3x_Wd3a18F^Y8&-ZPlAx3r6b9%%L373m!q9NPMl=4*~6of+uMeo*e z&d5RxESvF?Z!byZ1xASWizekQ%3`dnNc#N>PdW+PflU)tiMjYU?B}5oZZoQ{RC9J9 zkQ?6l5F|1RvH)Pv44D@)+PlzXqF?<1|6Fwlca)QXKxcrRjLZ0-BTQ57qwjo<1u$3? z!bLB3^FsL>XTSP1Yk^Dr_I5*}q*%9oQUlrk&8&c-)1#f2%N3L$}U{)i)fkuMh!QjTZ?YvWLMH50X7Ql<#XR zbRI}3k)~80y;;m579a0=tfF;sO7Ut)bYE0tvh@ zQ|9rg{9=DmBj>E&FnLiH*JF8N)n$w`zq7Y1tfunCoSKm6#Tuu`WK_<=ya*j&x^g7v z2uk@0y&;3jA!1X6h1YOji4%;8ZTN|x7sv;ZID7>(zen;l;~8UWyTnGU*g_;92jr~c zn+{FkJ0|dA<#^$Xqv4U0-jKn+Q~$e@N0S%m`~$Sf)&EoW_+O3|GAM#3>$jr=wrLp~ zb(qL-(bZVFZ04bsVE0P5(83w`XKq?ygp!$imxp1GnusU8J@x3Cx4zpi@`9AFpIL6F z+4g|XmtPnn*j@13L#`0C)gj`1aSGOJg98ZRk36~ITm)lMgjq=Mb;b0-0b#kza)|}p zWs^ZB^&|!^8iMrjw~)-d&1`A_6)vx+?c@VLw+8NKhJdgW2(PK!B#Ws!ExJj>>djKk zPXC0FtmStu*~!kB#zz;7eG$Z#QDdf)E3Zlc%@K4fw%yO#E>nXv-DVDbD%f}?qQ@)|4YxCn@V9j-fU`^rd3g3l&eKX z`UBCRl5N#uXS%EQ&h^WaM!-Q<`Y{WXD(3hcC2j*io0*v3@6ZPIzvlh6v%zQ)6 z(sJ(Nv^&jJXpeET+Ca{%PH|@Y9YgmF_6wHaI}9`-msiz-{=4s%*t$}!%t_s z988gAirazo)e2wVud6u>WJ~3(+0$35JFK<3b6TVre!{H!yJtdr;9j>glyodm&Oql_ zB1P*pg;?oHGAew2sbcx~{{XW8RNRbfm?OzR89ychh&V0<&{f9}MFq)ZhC@PP3F!=J z+CoSe$b!%X+9RbTB&r)H!)GiNVAgihcZLV&j-xg8u!Ww%|Ei=3X3X|W%srR~O>FZzPSM z_&_xw0j)d$Ia{sc2;w|jh}-)dsV$DK0h5)-4;7{_WEU`NCn6h4z|s_6M5ia#j}tKeCVZLn$1Hdw5Ob!x|<6u zkTY%ObUH2VZ7E5|S|$?IQB#<3OXKn9kteITsqOIrm!X_hI$YK-d4kbcj(kYbUA$e} zis%)9vgW?F3PTy&bIK9@di%kgUj?k$hniNGqytFWjO>a)at?8So{r~>FyBi)y^<>V ztUAW9)X)3JEOkb>1N1SJzN~^JHi8{%phfL(FAGQhx%Q0TP2MNTW3Cf)IO=2Sh&Y!A z73dFuAQo4oubw&Uvu`;rdPKU*jD@k+WFBzx>bV~gXQhVwuLMd;rgPPV2VAFgAtN& zwOFDY!?vUyw&2q6U<9f6Kl@p-tJwPj|K8#PhK?8VmxrkUdkOkH8R6XIw;G%r>~%sD zjxGj5Gy@^kB_U4Ke$yp3S!1YX3BrC4LT#;+jU+XZADi<^%zy9}sZjG#A}GWAzEk#* z-9<`)XH())5vU|)lUKC+B7JQqv1zQg4GD)^l*XQzGag#=LPa_}A{s>oE*P{_u@-v* z@N#ZCgXr7k_|vOb@Vz{kxiN%)|R?YJ68`Xz%H-J0gfj~hO;hTEk z`}g%G*tjhG*3rN?)But0Numu=ieug@N}TL!{15Ok_-yC3BU~~ZaP4HKAP)JtA5PCX z{MPp%M(N<(6~Tu`O#4Cns&HmGut!Ststi^jlSISo2^R~OX3u3?0?`I&O{rA0WbzVE zyj|!tD#-WeocZ6;n_S~7<_<`Q;Rwo^W&VLS{wDzjCCpICH5KgO!^sB~6aNxo%sOD0 zxPcZ!Ljvo$Ac)d*;8D`>Aijb}v=QM!82>xB4$YJ~?*y%t`p;S=aFw=CE{LLkud$;h zIfntA;Ut9rfk5vI!gGOM*1suZh~kFr(bmg1fO@0gC~y%Ds#^KEG!ZJKHfkvFGGty$ z*D5e=U7&kJrZ7@?9IH zGl$da4Ajq@>>S(K2gLQCpRB{gFm7?HablyOzj1M2{66_mkixL^aKHZ2!#z4^~6yh-)qxyG-Rc zaPuJvsi<|`<`fWeApSr>#O>LfKxvlYV0aoPDLxu~)yfipUTIPFt651M0XpK$bq2Ee zo?1jYCF$>$xqYxGvql(RsU8)ZyB?=xPizp+O;nIN_RM?CacG}?RZJB-^L8Uga3qrQ zH}0I;T89Z>S^*73rImVwep;5(s%Nz6$f1v%Q<^1Q-6WuU<~>P1`r`wNh*Mjfuzjma zhYkNlVJl1Np4Eo1$h+8K?rgOHO}2%pNmOL_Yo04%9pvISH@VZ*`>n#0_HAICfg*I+ zFhHiL`bN(~u9jA-cymDNO8`4{Y9d_cf_T=pZ1;sa0#{w|pDiGxty8Bw3leu0`T#06 zS$jbgngAe@Bx(eXs$OEl72*$W=g&R2uemspB)^B z=7L8l0{6wXK*%8#Y2?SH3uRuVvLA-FGb)PGknY1E2~(*QT!2R9Z#ZY@>-I3j8c9`-7PFWRY#0N_{Dx1AV!20Vh}l3lS3cr{ zlJnRe3dvf#PnWGAqJ6FN-J;8-o4c zz=1g5dksz)FtA=MU~M3HHDK!C36~)>m=zZ>oa~NBaF3c-!8PO4<__Mu_w!<(OiEHfc^qh?-Z?HS8EMiCK0wDVzKy>ew#nHwBJNFX>C#G1&xV(ydG{ zb%%m$=rL>(v21OUKKTnRgs}8}C&bH1nu8A(Ld;537S^~tkB$0|Ab{Q!*U!cr+9iiY zbv%38Y7ZpArRpy1mEciTc=!nJ6xx)`r-mdg6vNXe}978VS3 z%!YWioatdlr5H+9z#B?rt*`hJqv?1zEkG6&FEKY}^VH1sJ zYBoMI4_|7f6^=^2R;x|JmI}%~x?Q%}R5!7ZL z(Kq;(VQB?=Pwvoip$mdKIN>AV$hi98==jr0Wq#J!rt6E=?EodJCPDp;0+AFtCF~sp ziTdoU*wFipabGPeLp#>YE9;^a%b$j;l5Qh?^(j+S&4mm&TFDsG6HwL7I-Z*svWIfg zjA_=~v0vC3b?0*e^B%br!TzW+Y9-H@v&b$DYewkWBQj~m_u1VUFuiP3#qT>_vX@gp z9TjAZOWl-{i30j4JYp^i4CUC)kZv1FAj<5e{Eg|%b4mANSP8AH10@cwDKdkSiyP6l zxNJjOSumUq#KfEHGOUf1*@$cl=fi*?SNf-g)dMy~P49_vyK@DJn)w%FlsuG$i&R3C z%qZ$ko=^7+-OT;?KgPc?Sw74wZpMR0#b6l+!{P`LzXCe)eWaOw-dV;ps8CLZDVK8T z1%4Q8#~hxe%|XmziiYb`h?g-O3et?YlCS7+fQE`{?=3JR5T;0K2Y=7k^*5po3EXFi z)DqBD|1i>^3D2U_NT>!iupCk;G%4WQO9p-ERub$Qjq< z6eku(nga+%)**uFu7dV7){(t5WK?Mt7v-2YD`pDSU{l#N8V_m_m^AS;nuKF4zx9D|_L-wRdQ?<52#6+sC52Ny9CcNF|`ef|9m>EB;D90N8%W z?{Cs6EDM@*p==ZF_z}u_M`+u-#{&z{hA3}?3Up0C=btcrb6B@%5kh#bO*m)QO}v0{l>mxVj(9wGtK7J@I9$3PN;ID% zDs;b0-O(eju;SH{7()3@FVNm^S9*-Ej( zZ|dt-?gwBM>&ZHlaUNH5AX9NGxOqeF!56?Iz4%+=8r6x?(d$i6=Lu5I2{A{KZz5`z zD>F2-#6Z8F?aEjUM;SE;UaaWtOZ6k#33{Md`$(B$CqMM`q}K|3Oo^6r#`no~wRQaw z&Q5yk1HO&#?QGY(*q1B#ve)WG^{ex>tN#s2H*IU0UjuE2Y;Y*xrv&;5o%K_mRyty zL|Qj{ZqAeE>CEd}ypt7J>q2?|=nle-$&S>_I&#x?+pqpnssZ1EfShiM=@kOl5L5m; zOp}JHfJaZ~ZS@7e4Zu&g;!R}z4dH}Du05anbpLtQf}PY}9OzAqUnEGuG-yLsrsXTr zqz2j}4L29PNKm;o2WQC>adC$54F;I~oY+7aO z3BrPbN#KEj$^GBZkvX#)P_YY~37DV++79zplo2PZ%?Cw3ElH*nukyhoU*qVR*L z2LNm961$ZeJA2uXNl?y)CGliFl=NqvCFEE8x+PcpmIFW-;FlbDj?W#W?j z6SX_ucyLVMCEY^Y1%L&rkXSX^WbA4{ zzlKY3(W!bD8mFLTUu*JGC9YJ_o~^a?AQ&UEA2Sx57#Xz zuBZ-OwtfvpPYp#Yn#t-cc?@~XV6$8L<{U)j_FcLZ;8(5GHuE25Y8Q1Sv*31(EGfwU zpcgN)+iDrn<0;i$xVuX86n}E==2-4Yig%LApvcZR+Ju)LL#;#Q!NkS7bU+8VHYy+` zdNSiqEs-T6?65l^wd;UY7lfQz+lQ$Yi7@feZB6S){7C30*(4-ud@L?0UTG#9BBAg_b@c zz(0VBpih|r>`*DX!G+Oc#(C2@kqa%s{+eK44Q$Z(5tPz_UV+;p*JqabbJgx^m^X%? zPpU)ak~M=IZki*B2TO;;)DV;gUhPf#uVZlf;SU%^qjJeu3hudE87ex!_v>S{iLuoH z-1MGe817`J&Qu4O%)f8C>8~#`rcFqv5gD|waSCna8H5$Bk0D!vho6Os;HBT}zAX^- z=3j6bL!_;L`BGIH?bB-3aMK_~9%h%^LgL6%)4H_q6dT6+#nq2EJ{w!>DmS5|UDfIG?PI*$#x=FsE28^(LUZwAZRvWT_E{hE zB1mad!s`G+s#5Yfm(yOcEx3YoZz0nbdDyYG6}n-1FOz=QZ|p>@mo#> zWD>!xBlfMM#Shkj|l*#bIN_!2NQ<;Wx&s|Joe2U zqSl=kJcuHsb!1g60ypJOT04&0GSPhFIR<~vR3tS5M z`Z;V{(dl!}(7_iCPG0bv5Lxe;Ap%DPc9bV+u*NvCz9DZ-UvKGW_Od^Er!)`3(~RtF zW*J@kj;#vN?D1Bo+pN^q752MEn?resOZkgW+5_&Vb^ROfo;U0hT0GD8x*hWZUaZaChx2hm#cT8SVUoGeLTUqrs5723Xou1`J0 z7%WYh)$?QQT6EhG{)SbcB;*@Y?2Qyr8;Z*tC7_*J$6Z;jh+yRC3&~ABR9lc_n;O0W zZM+|W%dL#Fa>KcntP3fT7nIAbtaBBfV=N|j=2$>PI}`kkP1ra3k=O$lV)C1GsJ6I+ zRe5>Vw6TD&_9OId_G(OmV&d>4?`_%PoTPoO#`qz{#hKmVqh7J?HT3N!`=+dt_AH-m z8?)h@#Qj#5Z7o`+>7ziA?RfNUH>9U-4lfIV8-~MKS!b_?ZHHv7KiEW11c!mZ{YxE- zwjo3$Zr0x;EV*Sl;b8ix-GrFl3wa$!M*psT;nazFt$}XdG;m-7J*eLdDm}3P!X`iWF43gHgDczn?~}W zNEzA@|IJRxmwIkBaT2l*cPnS+uTo7y>q3vjAiYuvaBk8kH6M0w)Zi%_j7GwMTu&y3 zGe}Ax1w#-r{pN(fLN{~_CZeShgYC1j@}xMn!WbM<&kzjylTYf;FM@AA6~m}MF$?N0 zDM;+UbwQ8S;=qheMDiR^)oq4d1QwQJ|9qtt_XTUlDAixH!&-7~j&bDlD63gbc)Lk^ z7%X=LgnJe_-c{3%TKV=n7fY&O6-A&gsAxjm>5p(2mbb?mV0ocxqK)N7!I}caq(;#H z*ff6Fao~;mO0rLXnDJCZ4oLVVokOT%K}1$}OT+uDo@^SiO&=lb=>1YZS56Y;j`zf` ziCHziBv{wpd}fjIlpvZt#Mf;>?&qnMel2 zM$eG`4KDZ5+_R6qseY_N#STZ{^Kt5fyvL^mb?{Imj_1ag&@bd6rz*M)6(RevzIZ|z zkj#+mU=Hl`OCdK9!Eba^WVXR7FW;YHZbdchE@q&kZx*Z3qwH&8+ASX=DE7X3>( z!+T*k_}5iDo97xOt8=3)$W{Bva(8;|=?(U~P@Ds%i}As{)E7lWMnq|-5<4`w0P%!d z_cEx^f(IVa88q;|Xdrv-&Bio$Vi>u>sd5JTgfJrZLvREf6Ct*@974R53$9$r#buNb zg1OTyXHL7^tB|&M2U13_N70W8(6pUS1?^J_{R4(*(-FgZlxiNd8(%JCdll>snket& zdE|FSs2_rf4X!UT6t;TbdYCZepec2r%Isn@>VHL5aTWy8Z9<)W;s)pUR#5L8z*zy6 zH2@Re@H)2NByO-?&X7MQhy(FBmt!f{l~rSj{~goJ z`&#qqK@ZpoG&{=%bWwt20KQ5m#EuOQ+&3}Iw>s++wK6BtbNvl3p8*Gc)Oz(Rak?eemozUQ19T{hXD#tM|e~5Skw$B4)!3 zcMvOeJFc9mKk-$f868Zv`T^JB9#D6pEFk-((!s)SL-_>H!=lQ;U|L2o{$>eL84jUzv@f}O$E+vD&91T5g}T+S&@JtE5pKumqgP~Wie@g_my%vH>(8| z1w{G!d!AOHuoUDR4n>?DZvd*)O|9XD&`SBwE zv@Cid|B-C)0!7$CZ(xe9tW0M1CT?yuNqfrppdMpiG@Lh4lyJUY%^|Y{akuqr7%OQp z7lm=MZG;fhnA92};Re6TC$*9PLNQA0+=r^7RlJ>?vh3Tuyr-w7r_kP3#?@3jNYhVf zyd{D_oTS5wI#kK!=T`VTjgK%xo|1(V&<}iolBjynD~*SNfg(M zu%%fOwRPj#XY1$DVq_{~gwQwZr0j6QH_I#DsspmhbL8!Ivk9;7g68A#@SDUQQ;RqK%{+5`A)J~1eW3oyPXCjO z90SijBYll3Iw(>S6IbXRn89vdSOk{~@kqqHsE{C?WSICdYP0Y-z#^L~$-lI*LtPK+ zYRWP}6?6=-Xg(tfUdr+{A8$J3P86=pGY`#gcs(G*$PoWfJ$*#Ke=h#;NHy?LQ7Bq} z99Y3u>XvEbJN$eL6SHreQy(ir!A*lC+58l@Y1Ygfmm)85f+s;`Rhf;+JD&dVWL^JFQda=8bJK~f?E z!vL*?^BT#t>@_4;=S6&?K7)=v;qV(LW<U%t(^$eJPN#=GL%*rOBv zwR5F^vpA^~ZiIo00w@Ow4qrQUU^Q)325I-V;k6|46#s#{M-{BwT8i$6Ncy9|3@2kU zG+Nx$$sp8XzcyQSElle9S=C9XiGzb=o)CL1cx-1@wLcLW{`Y!vjE z|1mb{Ux23s?CALTV~k;ajHWfGb@zf1m=gve!c!U+}~0iC=)Hy0y9o&wV2);>X>YI2KnBns%ec+Zmx4JH||oXX(hA5bQqY zD2iu$!A9=1yNPn$SP_ct8CBVN6G!1w<#fs^4M zIRV512<#8}2DT9yncQm<_lDK29TGfKB|FhRb14JDvHZ0Mo-qB5L?ob6cB>O3F4Df^ zvnlRe%3Ei|EYYvYCo9=6v~5VsADbVcVL>20Yd6gcis`TB_$pEP#rdDxzIl zRpehEn=A74(&gO=hHV+rV=2h^WxUsJ8T$AW(AbH$!W__>e-MEd{nOLAEmvXVwZ4FO z!acmSe6J*nbtophICLkqJL;RB?{IIWG27ueB(O$%PI4vixdl3l)5bJx?T;onhi)_GFG$& z0B7=GokR{U;`3BkUh4*?HDe!Q^KYMA!LS)FMw+u+5eJhJ4L#h@>4uc=b6kbt?$JX` z^IS{0D!b`yZ06r&@@r|rD*6ZU{z&G#Fl5Pu6s6T>LBlwSH68j!45z>x4(5;xCwEnA zY%j8BeBD$vrT+r07sX>f>i(@QDXd}xG;YaCr;;~w9&n^JsX+OXDx$c_rj}-+JdbVO zh(?X?Ve&1SXtSW?WOb7L4LY;aM$LkNqYjUPG(9u9%uQn|QRvL~d)vD9B7Ig!nW`DYt=Bbk)JEwyYR9 zZ|uzWf-e_tlEZD9Sz^Hcs@IveEhugyc@s7*b7`F`%&`?ay(vYuZl=3AoS!&R7OyT) z8B+rBTTUyKW{Q)41Z#Ij{`@>sd;)@j@rXjn3XYORO?mH*@h$e8%8Ygs7uMD< z)Dotcc&w!#R;R6#a zz$qnQz)15iGC=zm%3t*na)BpisXL#|nhX8qsvH%jap_f2KGO@+5PG;XIEWe#g+`Kk&$Kpld~e5f)|w+?3m|I+ zvO~57D%A1^2G%n&EbhoDO~AH7$T|)@Lj{RJh8t_0QFVl;rh5csic6&q9iL`{0z;X3 zuXk=anTWqPqak(upG7CH&U31hn;637B6pQDV8`*rEYlgh!TX7mX(!XvE;HpOQ+cpP zP#F;6$0b-^bnAU=el?Hus7IqL(%nqO7P;#(9xe?aLatMhdCxQW6<{JLnhIKlb>g|W2Nqf5%Y{gFL2D?j)&-q7FMD0$JhdNEtvNTpd= zdT5$FeAN=;cmvtUN{-daXdv&JQL-9(bjnhVqHt8s5H@K-ZaQ&Oe%b#WWB6cjy0QPq zB$vAEc|-1w_7k6V8K8q)VU!i0z-)YgfaCDU|B0b&re>P$Nk=T&iZIzT^B1NCd)b<` z;=4S3;+>=z}#HU>U44XkB!6I zY6hlutA#$a+-qGG6{X>Nm!_7n%(lwMl=+u~o(D>Sl3!OUL#ZvApV;YaKW? z*cEKJBhmGtdVs;`KZ}#?AX=_yL03{Qy+JAAG<`L5$4?(l&X2!&a5^zH=inkIXg9$W=(U8~4|ns*69r*JyM+D^<&S=@D)7q- zbwc4Vz8t6X67jqbS@L~8yc|nKBLPFKG_^<0nrAF|)Bt=tIZN#EjL{@K;hE;s`nxE? z&A;aT7J=y9Z zx#YTk^wDlQPbeU$0gIDIR`g%DVvt4-5l6Y zMzAyfA6I7?6<4#Z>mWfJC%C)2ySuvwcXv%ENE6&?Ah=s_hY;M|T?4`032-}mpL@>! z_83k7sx|1bYSyf(DeseRJh5VKJrc*Ap3cCLjG~RXoNPSL^{s!qBXw~$t-f}bOXti+ zxunM43RU-_)!@hUx;H@N=QmGySsK*=qOuA!W-T&ZNcpX@Hmed`iZ(-JR)QdWOSs&Q8F&p zrNnKPas|Yy|4?s%t|UyLVYIx$Qc!YEDMm@CbEt`*W66VQ&5Yf#7YKEHa+VF8zA^p( zA2R2B&b`C~YYFFwY{_Gx!fw?wp*U2Y_Fd_D{;9>aYhV!73C@~A@KO#Wws(^=@YEry zn<%Qvg!ll$@OIvniQDwxE!85GWGqmIzTYm7`BjkbnEp9g-x>FZA&c&y}3%6hUUozX(T zRwbqcAn+c^fAAA;-V@OZcE=Tw5R%C)f-zflYU?wBfx>3RV~{a2xxND*9KU00LE0t> zzbMVPci-YK<;wg_EC1-V9fLj@5`M>T{lV@+SY>;F2 z737cMA6YIDSZE|Lx(z%UIMor31nP5hqaz&QKLY??=0=f`A(vS{Sg^Po0zSB=9S#%h zIt1whiOGYB|-u#b&O)tAob#(X^h|u2%wRz3xmsmS}^d9 z;n`ldfLBdpw3DJ3S0h51(9+@&b(0Cb%bE=JntQ-ySj2W&fIf>8?oL&(Jmx)^3D3D$ zzS*KEest7EL}2gr&Nx3^-C>R9Sppf3y66brGAbF1{u!*w-g}2&t!7-vv^T?o-9ADr#kC0$8?2NAhXd4(>EtU= z$HGDp-yKj2Y)>9J3vHqY)4bA>acK-`1ZiwWFO;sy@E-ZW>xXJ%pu=(fPpFi)n zM8=r79Ie!Qnhd<>+$lqh*XCRmL%QSrnTW`9*qHG4C?Q2v%~qVh?0agDUz}&}(EREp zvjfK)eSd#_mVgl83x#lmfuJk`AA?Rt1MBK@zbe-8tuMpEla<%GUjgfhT!Uz z6HD3A1}!Lnvu<2L=d~+W@l5lvH+az+w^&dJ-AQ5L0(JdOPKHyC{r-npeoC6P*9P@A zDFHilwL`4e%R!7IgAu^&Xs%3r9rueHZnPu6K>F zyM~M9LixI{sdTf4s}^q|251x((}H6I9?ylN;rOzkb-*N1qD>0BMZq$iO+KG$zNJak zsq(ox6Xv6g2rO#>`WI4lPYG$=@)ErVjxU`!VCpxjbPx3u!UxvMVR8)d=HBDLAw=o*i)Y)0Fn zJrD8X;;Nv(VPq^xMfm|&#@cz=ij`iS^lwr`Hcxjj61#?%_w}CU#(rD>X~t`@KLa4A zu&6u%b{7eojcFkiD7xh@_4{(cYh?zTrMG@fI!4$yFAWLhGClg6`rHGo#AcIv@VVxJ+Oy8*2%YIgoc*|OBS*y0c#-F_TVb>}R7}IFO9(T%=S|i!m9!&=t z-tOVNc-tre(CS_Z&jfLK<;?-AJ!+XnO?O|QZhn4JpVJKEGYG6O$7e7e=vK`O&xmB1 zLNsuiRcfXmK>FAvGE4DAo*;yf&~psw2cL9><09g0TAqF{%GlkzVOP~>P+veWmFO*5 znXBYEjBQ#kaNIR}@H@gqW=Dvr!8UwyqenCfOf)*%qE`>JbGV(YULWzwqTJp(f|rti z$PEX%z1s^>S)62zKLVrYx_^xlEV9mM3odA6ON2zt9kDVrI@}WKM?zsd!wfz14Yanl zIElm5r~iVB;M&ht3_~^>KL8|DPLc2)_y2kuA!dmPc5s101dE|T8AA0pXQDxEKmpU# zo9mZM)1F1m?b3Q_0mvncuK11|O43>})nfqgb2OpLAMP$TAOWcs3~}c(rYxY{WzR}S z^RQqQG&`5WlkKLN^4roRYb?;wI694Sw(NluH$;TAwpaGX*ebcRsiu}arDAS$=8f8@ z_q=! zpa9Zgs!t*V4}7K_oAt=}$&(!#JTZ?q7%y0Zg_?~U!Nc~)f-(W@@q$f zB3M>H`I9i+I?+C@u8I3NL1HU#gfTv!b2$Zh02Y1Q=LC}H2@#I6uS=s)Ex;vCp?7iU zz5>1?!K=S0Fm95gsA4o?B2h$}PywXNu&)Cbtb+}YG-Pr2OlU&|Z zga}W>XsK&r$)mr*Nto}HmwrwB+>>Q1m#i%wizN-U2f&4TUpqoeJB(!Ep$ySq=F~c0SYvnge*3Zvv&&P)^Pp5CJ@6NWg;Y!>@L*Vta_wzX7lJzyM zjnh%ANEolB(v_`V)%Wd?im;pT67R!ke=z`iT|b?DCa>LIBm!|-ZS${=`}ZT>`Slki zd071%S%)D(xD^#p8luGDFk7pSDVu?ihv8YO>XM6@!Cq_m42WHxUx9cKw|Cnoq! zN9I^=n{$bF1+Y!DTqrWO(*cCDg^%8b{%&_P-*2(S0}>hN(hS;COq=?vk?Wh%5uWTr zzBq6(IfWl2Aeko0)$2RRXs*`ori3Y*L17fzg5!@V-&bax(8X=Rq1l3o(UCWc5k~im zri6uR1uvo=d+2AWK8|4adMF3gS+w0V2G$4Z>5i}9hmHf3224E`eR9kgTyZT z;S{RqqpS&7ME23MMWHV%ms^`or8yxoX`Os3nC9EUVPTtP*;l30$QJPpJAwv3Ne9(F z!*)xp*#TxvhuO>IGYo4hv7qM0L_385@*RH(yW>9ORzfGsow7>QN++6fCk!7iC&hCJuHmS}n198+lOb6#EwFeylpL6Z5#o1q z_#w=`ADXKQFzo2UhgQwsZ*|U_BENq9;CXpfjKF}PQ1-rP%fg%2euRgQtE+pC{&!oN)wr5mf0Ukvfou&@Pu>GC@Jm7cO^&IVM$|AgLbNRzmml_ z-B|h>&sXtj{q~;(pM{wYkup&K35tXNf`Hd$3UpMY?j;8O(rBHCUnwC3gnTHIYFuv! z1CBVwsUUqgb*-KV!avfAOxb@YKI)g+S zb8FLbQ-$X0-}dEK$>fEjZmxuvj2+M7h$)&ejWfF6**)^o0?fZ!;`Gu5LQ`27-eE*= zLd$P_3dXB7yF?blp~Q{al~>fPSoqZ8F};EudpxzJf(V z&y=n*z4#(XKdA|T%4?*EH&JC60l?#euq^J^_B&aDfpG03#G@WXMbBpShyGtS7>=3g zQ9VBqNQ;KI9=0;uVI;2FUYQy%-I+loPh!H>Yg45?d6&J2gB!AOah{$Ey{cSF&G+S z)I6AhhwsVzB@6*kU+@71?K8D)&KHc~t2>fiZ0eO~*Eow#XLGmTyOLCb(uux$nvpsE zV0bar-=pG~%xShBavW~!4Vfxf`_YcDF|r3^q)pBf)_m+A!_66A)yrnY6^)vBe4H{> z>&WmBaKydQH0)YU!V`+6CJx_*H;b|2E;)&9!&=1fGA7N)UqO){>%X1WbgY?NEmiZ| zfL>@?Z6F^6a3}D;)ape~#oK2gtOW_u+2ZxXfW|A%BtTbIfB3u2mVS1IjMlg{S^_L$ z`5sDc5swFte`Ki;gBKfv%*uOlSj!b1YHU5hPHs?mP%>bU8x#d_1tEn;!sU=rZyVtf z?-TjuBVa@;l*7|;Sw?+^&3RTp^Hk9R$%WX>|Ev18avagN43W+BK4rtSMLTK+2-2>( z(1^eb3YhMEE-XlzzPo3(65AugeH7&I2(^UHnLuZfhApz_MHR0)tx6oAfTkAi^Zj)t z70MnVtDYEzT_;1Mk?pCUaZ5F;g>V~wLqQ+{05XHb$cA*sZn}Mm~LL%(L2Lgu5@(YcyVk8HKO7KN_ z=pvw>b_RT@2{NmgF{fjCUuDWSo!qH+e%2uUklHM4z`qV4I&1W$n}4&cMRytfnz#I` zlMrsEvEb~?8GXR}ymLKiCuE#hRw}_!QFVay^l9s-7f_&d+b6il)CN3Q(Q_@rf^@cs zC)0Y&;-olfRjN0B-!bSE_iT7qaTl=N*BH`!gUF232ufv%P)2-V3Nhr>n)hFQDC^hu1~4=gT&D_9W3V(wP3l)eQ_h=w~atUBQ(A83M%4* zoVni=fPeJP+2Yk-m#Pp1#alFe%<&(V$8CwQu_5xyi$216lPp(M!&}=rKC%yPTzW6G zY`s*&M#%mWUra*R&i%Q5=|$2RR-N50EtqZn(qWOc_9<=b&n5PdM`C95$k(^v5}5G; z!;~m?@Nza;_=XG8pv|V)53P7y-5 z?UJFH6E-Y`=_QCj?(oMWo3TY#VY6&yAQFl8SODH5zxH*^mke1fl@OhuDp$v$I==}S zfc9wp4K+wb+T6pwq`qWhUCMe=7U;zpQ{MqN0$u@VMGd(w?vWkR4lfigSTY~@3rfzJleF%U^JSaruhc0ozGAZKlc-N;7Vh~L zsA!nkVOsiwGg`>})WS-oBzqd^cIE!4^Shz;BAXQ!ht8t3f36aK7>r&YU3zX3rUwBN zkLR)M%p8)NvNaEa?-+VDv#w8n{&Vpaka0u%K(_W7XmETTlr5Ba^J5*9Qpc)nD4XDl|KGCQ@BpK3OS(?GA>h`R*W!MLv}b z%5(@Qs-%v~psvZX8U01ygzI0lcl=G9f{2lB<)-?qiEoR3%gy&--FJd|mk9&;aU7{a zT0&a*9M1}j0bTLs)s6Tw%Cjke$JCjtK-a~D>6C-eK5Ro8bTdOKhGk%)9faOu4q8v0 zS|2K9iGzoLK$u9+F1u_Q8~4Ux&z)x1Zo`C=%pRr^BzQ|@9i=VNU@AZ(4Duqhx2y%E z^D)OCX-iNEK6XSrPK)_SF3Y+5c9+KIaV#Whjj+$PaWp}dDQz+o$ZJ0p5QYHl;MB?D zv^f%2HvO9Y&FS3Jqy{9J!+}Jp_)TxZ+#rdaUdcR{2k8=87$iNCq_{7OZ@fH}6w;z{ zS4vZg;P*u(tSUp8))27!G_mS*hMOh6zJUiNRG<=;wVmj-exO1zy@X4U#Wf<<=3eKq z2KBd$dOhM{wy2KwkZ|bz%G*1*{l#mjynNKxww%BQ27j(elaSVIp1hzvDGs@DqUnuuD8F^F)m zhp>>u-^kC+QN7!ZB}Bd1DbF6q$5?#TeY>OXW0m}c0sjx@PnjKtm7G$QEWq9)SSkj; zja9{VpVlO*ZZV+BU#SihdBhjPbZCU}g-aASyE~^Tg0aoU(2CiQX`c~WBKUJe;sh;zaY}W<4Ixwh zdcSr%9F_O0jO@{bj$iT;s#RebBg=RsliYvWXC}@%BI)|WOh{#3u!;TpAk2*1R2j%# z8NTtVurV+~$GbG8w*XzUHn&Wow5(_m+QrQ(sw5L#(l&@IG1>;o_sUgdf_DkLX2H3x!v(V<{Cw6Iel^d_b$V5_MPdmSx-n_Tam@Xk*?E<9&lIc!<%; zCmrpgWmAAsG{L0*1du6bWhp#!;{M@tN+`0Av!KqDYnK;|g)C^Rztme6mH|n3NpHNK zE8vY7EM2yUdRDCwGDMhplnL&q9|Y7Y+f>{gIp@Vf{}6lGJ1hwn*OlQ6myxlkdPF&4 zROYBrN`$)y){0MOP;ZeafD8uqG5sRPz_6{5fIadms3YXr;|kx(#?!lApnoN&APVj~ z*6%4gEo*&`_revXV6KI-;b%nyT20F&1KC2a4vAx^a+!)$dj>FVHTpld^ju@_W`fFzM*Y~cT>jPW&L0oe2M zs18wV$>j|z-7zqwQ_6L6bKhx{J$1@gS~4y+5z$hg2cohHVO(~>U#Ndwj&V>cB>O@= zNfI|X#$Vqq`svd%>K3~UzqOm66s$!;qvK50eQ z6n5T8XzaOmkPP$@8cALe2M5>)VO|N!+h2+S@Ut6QH>04ZJwOF)fczzVQLllXAD$+% zflNEY>aG}94>8AnNwK3Z&C?qqUMKCBW;S@f1xpaZNUMLvye&K4O9h~yI)9M1n@$8L zVKqj23o9->PZpSi@iS#aQ=JceJrTgg(V0Ke=q`aNfe@}d5^nf0nU-hgo1Y=<6 z`z~(cG0l)Ifw#sY4`H^w&`r90J$IPN(pe65t(u+9G3hM6aKc0O4( z)}#)q4yZDemon^l&A5aMjc-&?TJej6DRLqDGeqgj!NgxR@*}z+)}S95-L z2EnUIGRG0|)Gv)#5KeNR{7M+8;s9|_qgbJQ0F5yYdIVd5@C!UA;zcfhvn@>6^t6YwBq*t&$V4B=g1B(-lU@KPvl@n|} z;CjNGh8uSJLv?^Z(+r?+xACoK(g2cP`-n>85aT-+SG<{&>U&)JrHU5K?xUO6FLv3i z*ol(uuvp;}&XWzxx4|VZrtZ1gNEoYf0EAEDg@;qz-dmH%76d&@mJ#AiR+S-fM;sN~ zVbSNw>0ByJ`Dl#|jQItnwS!5O>-L%HrTHkm2%Lcem!(7FSDe517n7aPBV3ox))M_} zd5iJxT8}KJmjVf+Rl&BgBn7#Q+*(9+nlxsbTd1oyzEi4>@(CkPlR@Y8%gqsb{VcRY zUe^^u=m$)x7KR=6)8DpJ4hdS!0x~-CC<*C0D|n35g!J?*uSqQ$43f69(5r|=LjC?} z{*-;9&;P;H%7ExDu_qE-h(k8GK?nky0>%{hFFx6;%=oy|XkEpWWZa4*j0P;eIN}$x z_w=2URY@_G?V~4j4>|saMY-Xh#6%ec+GRZb6bRBh9{1New>vt8f4(ctbEFJUjM1*t z*K4lnju+MW!a)mIR%jfJcL)_8R&y*9Gl>CJ%@J=ug(e&1(f+BLVz?CT%52qSmjsqj z2WO*d#|;+HAvJFA2C)dK4%E7*RF6@m`d11KtNsi=rC$O#_sZ}^hqlIdit;B>w9GXM zpiF~{$teiqLQo;>z-ln;zcHt z#!5`_-zQFRz2m`=!yWqgC*{xAv6r}Cr!K>abcr-3c5@^DL!$l*qW=4O=ipxHoglBb z9{Mj7_Fsf?Y_KT{GywSTD)cOMWz`-r?94q)#-Z>?x?G|!AlIRNWIl+^o7=LMcJvc=E8P_! zffE8I5|FIXI%5kZ@czg~WdNeC`lGJf;?v=vLAzcTuyz7;U5ww1vL0&Z9RQRtJKq|w z9voY2x;ZY+($YTlp`-bSkAyiqtMroaueD^xLn}6LU z17V}$$G!nC(eM&yRCrstr>ff}r?2rie#+|bO36!7kHk_)c;;Lo&M4Y-)yT+|q)Zc3 z355t_Aeb6sSaQO|*gL9p!I4in6MYlyw#e`2lmJ~Ke&>hARqbZ)dV3O!SCZ6P_xs;FEX ztm&K01%ZwWzWCnEk>!^M?Pc<U<}+2ocE@riJ4 zS8LyfsWMzUJ5b)0N&93)<+0s9bEXANo>()#^z%smn(XuIC&D{SAKHun#^825;QL%- zSmP26{f1}R=>R0Ua4rtObB_1xd@m7AO|~QnA>bDHf&}FpFMTtNFac*Uwaz3HTNVsr+{@((Usm!FYq7 z3XYQA(MJ;74-ZT0@nn<6;Y>r}GQE=G3}p4aK8cpG6&T@m%mIx3eahW$I!7v z&RWkmJSKrn0cYvB9|iyLeU`TOeplgE4-$q-Hgox)(#uTXjFL-neF>}000~+?Bm;65Ty5UXx_P|w*>+A3D#pBrb!9zEihH*56~iN)yHI<_ z{7@Mv#8m%6AOmgNJ6@NVy8PuG>WQ1!^~Maze_iy%q8 zPWs+v%ZH}QAX&AajGs4WFM?1+r!FK>M0Gv~Z&>JC*9lq|YFI1eacpGOI9^K_=DGba zlTM7VE#zcUOkF>8X4Xu@{K$_NLBoTWI_*O9!9ZxE1wVoy>#-eZSsm-bp7>JygWLX0 zC+&Wj!X$P^y-nu#v#f^oF({UAWTszwQOkGlv_qP*YU)h1`1~_9b*Qmqs2mGV zn6S`zt?NoYTcU{S+OF7sSk>jLl`0-*{1jixeW_gh!R%K(5X13K3Lge0tAL02ZrobP z>|5f^uoTl~6X2@!^h}S-qmtt+_Z!ChMoDt?O>9n3fKx9|>(uT#QQYuPA$#f54@h9x zsEuX~a7CAK##ol!X8G2!id5;|+^zbkP!QXO4NWs%5W6OeDurWT6Txt4V0)BEfKBrI z-9p~aO%u`mK+v;N?@End!dj7Q)7-&8sr?S2`%xcPK%ls-=?D5PUmD9A;qyqB6TwY@ zB45MTbc)p)2|qQ*%H=GdsJ=Jt7nwQt{O%<-m2G3fXsE?Lg4HFf{T^vy&8CC~>ijY6 zjqA|H0Z$R>R^Wg=Y=6@~f%cw?-pvq#BUco&`acr{-F+sn-(k`E`0 z(X#BVg|QEm0W`fdie$Ixh8(5H)X)NOuIyMA?tbj&eEe_7#H((1zrvVi+)y{zAFrdl z@D9HH0Y0)2n`M?zqACs=)|w2$WmZ{G@*MgS@-U7OJrjK=h#sKOcn2)o)qQA;-P~%9 zUe#djQRM!kGI)Lozx-4XD~hPe+H|3U{po98T=mfsh|}v2K5(1T_{eG24X)|vfzf3C zf|nRLD9ZImbMWVd3-Mbw&`Zj#GnUY;Hui0J1?W(BCvdGnZ=6!*PW-Y|#`gOwccMLg zP8CD)N8OC_xmsj)W$i--ve9{kMYr@^D)iZTZ0&-IvT_~GSx@b7MlWt+-Qf{{@qC2= zF)4Y4y&9(%cNDEQrPUUkzJtbin|*?mkeNHXRw=QeDz*xLzF%Mn(p>1;GxsyXa|KPX z8nC76S7-xqdXWRiDX1q=o)-maQ1xifx&$-xgpe}K!#Y7XB_-Bnn5OM3Ql@j?c&*FV zMVhB&k{W(BJ|4bL0e=whX;=wU#D4TP&`cEBh6+fXZa2!<+wA}Njus1SSy`rUfDKSU ztZu+cFJi~6^Pj#E8{t&jP75BvJvFO{mIG3)3aJv}dh%R43+9l1GI=u%IBZHxv;kY1 z`pEstKg7*6`o^?jJDz07HmRee<(v1afo_o4@mahCTv5f z{6F!HMD8x6fabS1a?9o8*9EJ6Pr)k4?FTV$EW;29C1UShv4n{g(i8n=gDz|J7-oyM z-Ffx`QR=T`o|YgUG|V` ziSW7z4?04VaGK2XGBrmD)~z&HE!M!&ts{#(7_~fEwWh43D~t!e>iAY5ECKL$3P&}8 z_~&c;^kd32ObQF-QVUp3$-(Gc9?uhm;xK|B%2LY3n^SnU0?L>tE`lR#cLd_TN-ly~ zTLN)Te1oR&yMo7e)^WKh^$UP=_0^X7fqD-aHt^`xPyUz!`Qjq>xIXZ}H}{VQ1lOeb zP*!qoM&SanhIU$l#B*UdOtQfE*zn%RD0qO`kDwwiaak43&tL9vI(i1e~w(b0>UY zmJz#YDH!T)`*eGc2zMe~uv%P7-=tmr$n3s`>bEGPBtd%5OOsFurIP;4MI=W8<1swA77&(9Io*#h=%`f3&b zRFG&ZzJ6mH%4Z5_!#-Gy>VIZ0`yDm#>xY@&zDJf?H})-eUTztn<7Cym=*=T9f5CP& z7Ua%AoSb?M9p!G)JBonV;~g4aRR_rr&X$Z+b#|3QWt&@ipJ>I;%;nCl<<7+^&&H9( zXAh{#fA8FeR|6$5?7nn#We3BeROw}~k7dE+5>S(7_APD1UFDEbjtH#vv&@Xmt z66??VwkPQ^{Q*TLQC6(06ruOQ5WuO}(6`LdYjqK`t7vU?vHj|qq7N+09*3wz1NyHF zB1YkEEOw!$G=~HWjid=x)GrY~kKX<-@bmYGDQ_`?$B^*5tzxC!z^y^yDkuZhDbX zzL>-}&);L|9*+?Dim`K1(NEBFrox|Dq=MBjPNkT>(kn5hA#RcH7*DZPY^l>?ijR?( zZPf<*fl!;x=V)y3aq89>_?y&c>~Deg^WH|^6Xv|7hqny6w{?`u-QAB@CKSyw;HA62 zujx=ov!?<^UBUPo2|6KCK6roZ+NtpT>p;|3x9jI`4FreiF%(VE0Koqz{7|*Y+{aTO`6>3_tvtuoFGx z660Ql5vHSDlM1O$i)iq^Jppi@kv8*eu!>#amp{u?fji4-;oXp5I0)d_mZC2bDqk8@ zl)_g#yWWsbAD1weFa)fDosyT^SZ!C^U1PkkA4&bipV&iH8vN)%bci()Uv%YvsyuT8 zC}Z3#I~wF0X7zaw!-p;KkYrrLsT3ANDT>tBZYfmNS}kC0GA-YCHY3qsb7bEzw8gml|XFM;qw5wxNkr%?nwn#xPR9Ikc#7fVk#Q{6NfK!vqGohrC0bk4Z<48568^S=XdHjN`*=AXxT*-TB#uiUkwxU&s%yaV!lI zb31FB3IECL^sG(br&s8jC~ri?mec&ND7YwVwP#f!%JBC@Yc_N=7Wlk5Y$ragNHM9v zJX}>;EGJBtrr%jnYVIzkEs@M~^>+E=4j9s;LEYNtr)L;$LSO|NR=ik)sKgThJ`yo8 z5-0^iCLh09!?o^u`5N!93ok_APU)2WQRREj9(&v@3Y<4^M2%nG_abkQHl6BTN>!OGwn({Y-{kHgdXjJc zN#lJ&Y!Sh&6vskd2)q?jffgw0l!L@zv4`i!L_RnB+=;e{RElrVn-Xk92r(!G7|Kb{ z(m~$(1tb4_uxBjP?(WS+QgA3_-6uo>n5xmNYV2wdaWzMP7wtIhPC)`DVs=07GnuK2wvoM5LrD-HqWl+e; zt0Bzm#@J8rt2%KW(XeOlw<$8wxcT9KC}RIL6Xv5i;4#EZcwqn6VHznT1MRWIv0rJF z>#ggX+`emTvNp;9QZ_;mj!t40?mg4SCjPG9QFz#s&`j}>2fVkmS{}U7uwQ^>en%dWp{(G#}mu>xf`F3I?-foRtx>gQ-w6c7^#2$ zNSZ``ebFrY4q_Q&>fgUyR^M%Zj7Pycxff8Cm*o?J_+0K0!^nN@0*$w(%| zcv)`8hIo_;)VM8n3pX%13fNkNF>tsL0lYjiaFa?lRBBjz#(Hpi3oLr*6)_It8AUMV zJ$_5Abt-X+M_AcrHa+-?u;Sww(3GA1nrtG8+x;9)ir;2Kw_W?;L$!xDBS<@vHnM-X zocA?{xpKCsTwg8t{VOtMQC*f`II>sm8L+8f9GBNxsaQrc=^xqa@QNg|~+fM*ap7Yk%Vq zhVFV#z+x7a_6dh)s!|L1AGP~`?O{(fxqJ=^QaHK?^ZyM3EC!E=kfbhy3l)=OCler1 z)!I*_nxeDJ`Q>|~pZwYVd!$twnlD97GdJE1xCy7jbY_JsJTU1uMcNZNU*6Liy@T#{ z_&)4R%b^^RKf)M2Bf9}>5yGF5gdj)yzmijb|NZ%9y7M6M46|(Zo#djU>T55x*X}&2w5`4e(viE+X8 z1{;5osbG_|7I-LGYnko~b32XW*g!2{k7Tcn+Gyf`IXnCj6cpeOR}--Zo1ezb1{OoO z_BUXwu_YTsUr$QadIoIo7-W%aG_3}cYp100-L@%j8(mn3tITM(_X`5O&g6cep;8_x zv;o?+og0Y_o0J!iZyjaRU!3Y$`@icK2<}L=jCl0rspS_rmN@zZ7-<)lyK0*cwMk?J z)OqbX#4#y5b1LChEOtzeqc*dQ*Y9&1GjlieRtkoey}ZP8;_bi;5&-ZIQRTU~siq0a zu(($}40iO6!f{^xhd@AUDl@Wynk)5~kBaeqd{^?8wS6_$n=9-}fxIfj^iR_H);u-Q z820TXW=DMbFp`Q13AeH}0S2M*x%xn4vT%eeydrIPvV1-nc9m33sIAY*2817h z;*<`o*IGSn_9r)Ue!f#O6A8nK zE=uU}Wi&9OVbA-S)Rr+bKTB1dlx8Nk*utrGQ)G-IFJy6G*uaDVTSrG+cDE=DH2k%m z*o+N@7u|7;1YF)0CxeF5->NzdnBfVjF}Mx1S|}dRzz(1h9X8M24m=Lvw!=!&Epu;V zm}k(Hd{fW1u+0WnX(0nt_E)rIoAEJ6=LV+KozNaHHS3rNHG0Of#cNA{9>U_!>(hQ|ZKgSP@SnA$i3EGs+1_E_C1} zLT5U4iHM6dkV@)g%^t@YeU1Xe`NBFlg+yTea4D?K6FrX`RusvQ&UQhn0Rng>Gpd+d0HuP}?Kc!C?9?b^2pqEHQX0R>-KNs_<)GJFn!i2Y2@W9-=iUN;#DK_v(q;}y- zW16JaH+|&PpjIe=&x5E_nO(m%q3aXG?$CXirgVIC4dW=FoKVcth|C(cCy?dq%;Ze6 zuo4sGC}WjENvT=IKK;4~Z4leF@GSXl%GEqIb5yeTAC+!@J=as?C~qjla|J>+DfNHn z_QA64P!fMv0$kEW{J&cqdLb;kRmGVFdxsw%F)$iL@H#r^gOMccDuXpH+>$E2sFf8G zWdbj9ZsgT2VE)j#VLH%9?noZ?oQ<80W%CQxUqjMTNT$e9{DtgA<^tic)>>obX`1Yl zY(AzLJ@-ZAPWWr{Mv7XQjQ3MXI+AR^7-K0rD8D^-Uf_2Db59^-q-Vfzqk9&)mlQ7^ zQk$pL_jRsNpKg?IDhh25?(Gx8kRU*K*_Hw3Sj5D?it8Uwbw+QYnb)Qz=$*MY6lI>p zPscnY?J&rjmv_Xq;j~FkIG6-??sPY+OV+AyIgdt2?N*tOd4+yujAwzo?bi@EDa!DO zDcntk@PO{WomV;gYoqIazOwekd<`Js2dL*g4SRAz8=IEiVI=202+spzp!UGsBwIP< z7KrrrJGNA|8ed9@uM3@$3OA(uUl32&dC&$?6IO+c4Z0BQ9#v_v)u__@=@g7VTRM(* zlDWx~4TrOQ#U#`Tcvpwl>+6{Fby40@E>%vGE}694a;HXY|Gy3v6&{)m!H{V?gZ?+F z@c+ujA+pbKU_yS#-t%2USrO8k5_H(?FmKSfpyQ-EzSaHV<117bTse*gU2&}`IVh25j|*p3&s@$) zQMDW*1^j)(T<+wNxSMTHPL(VCr3pdBuU$61W4FCyap%h%@Hbt$+HVd^TfVhp*v^xs zZCADn7^^s*(*$fVkB>11sE#>6!+5!!R8`r2?qcGP@K4dLBeinLswO;|HVJ-rs#s~= zCz4*H@zyO$=lhn$#<&&S9&N*0xW?4bd5fTS>evfT*=_h(koqdGJ^rOW?JJvWf9G7A z&ZjI-Wi0CipbW_DMfv3;wGDg39@AAR8_}QHS@vI@_FS7Spavsh= zPXF#Df~+%mu!|)WA4JBm)DnvQp9hPj`t}a)HOn9=LPakHGB~a zQaYu%FLC)DCwjBOOX&UV;;^t7QW*XBF#V4>rJJqtZ4L5^&Z5r_laFVQo!37m-U5Fg z_f`}0qnh%2_|^c119!l{B;@j98L zIw?Tak*AWF&V!4gJ=Gp-b0Kcc1s+^&W41?{&HQ#`8PBd!75-XFTT_^&&xgMT1#c zzQe+W7oYb^buUOBnch*neo?&K>OH$Q-S%lK^=&QguiT>aF(GZ2lA5^trq2uf#&0%R zcGG<0Bu(pl-nqZcEw=K^r>?EJdn&xcRei#Df17o+at(9MWlZkxjNYDDePD8B+0RSU zL%*MK5_X&RY4P2*L;eTvo|$;hyhmc=yDO(#vlXxHQablADlDd~EwAoPL)=nd+or;t z_KCAw>Nbe`T%RTFE7?)xk|MB?Q)uy}6UXK)^50xN{xHhhu+zL&+xA>Rg7$RT*sb06 z8&7FmcG+HC>32**c2Y(IMJzPt{boYA#ey=^jOmTCp>+ zXvXK*+&it$o=u4Q9Jw#Tohq8O{t4yYJ9W}+uRSCE9v&@CDss`U{F#^=FH-n6ya?5>3w1np4F#p`uFZW+tlQ_S`A+p3(pv~PhZtE4< z7ooka`}vMr#JY-a$Um($|4VE5=?OKm2YSXV^h%V^)T^EPd6dh^ptS!QJ(L^c?5Pcz zzFK<uCCAM1KTKz?- ziFpNc1vlO5mksLdzp!m~r;F^>su3n@Hpp15(eEDHU%ahjbK=u){p+?lY!tr$24#MbgUb!E-pK z46>D;3d!sZ*?qWhj-0k}qFLS}>2AG!bDnNHI&GtjuN24aPqH2{->crfKe)2Oj4vb5 zrTxwl{V{9(XJx#(nf1MH|Il9_lsD z)ZP7cYrq+~*vThzSFf!ssTfx4l>__r;4X`nMu(JzuE=6riooIDqI_u}0r zr%SXQOSG|$k2?``^@Q#xuCRn@r>EHUy_TQ3bYs@^a04&X?yQkt6Go}7IH;|8Y_aqe z)8cIpAB{{talK4g&_%&^FZZmnrH;mzjjr^R| z)%)E}&0m=DcDJ?W%ENqHG(NrVog3i%XUdhdBWKF$-Is(Vwx^4iT>LKn?1A|=3vFDtA&x_-V1DI%+P9oIIARiPgHyjo%Ekw+5C|kT31QAs42vYoep2TegFB^*Gs2N)x0aUaKXuk znH9^aUEBAU?^dpU`(*M|vrCtyS{2gky(=zFkNzn3I_CY&$AUXMr;fH-lKAaSiqXki zrt7U*d!~6RoxF72_o;j5Qaj1})0V}5m3iaYv|h2OnyXEHnv{moRX0mN{g$Ay9-XUB z`sxLAE>)^t`{X;%Lf2BGWS1bws3X6+xd%T0Gcb8)X%MDZZp&@_F%qKbhcfQlIXHjKQ`x$+ORBhh%gq_~d7)9Z)q)?)bg!8nSEx_Zsy~%D zG3VH(>Ep_0cA1Ms%N7@_1gYQE{ZPLB(jEccLZ5BtI*a4DUrbj1ZK1t>Ki`}0Kg3j| z%+t@POuar|en;9))nzp{!rbHCpM{KVRFz6~X_*;t(7|1rT6IX=^S;+xiA|%&Y}@2$ zHd$x9i>P`!Pl=2vcxc6P#}=g;ky8Qv8M zSodwRJ6EQejP%)O^((%&b@t_>7m;SY6~C6*1P1RAeX4HOF{z{e`VW@3H1MoRu$}Un zp8$1PpCta0AzhdydMt=rG$(H0w%_Rs;$~Ij8%jTGJYVklOl3mFjI=O$=@?;4v68)( zerg^k-c`B#=gxm!y~e4@_fTOfhdwV?*=y(5d1u!*H8p8Qa%);>ZfFpmd9in;$D@6L zzt7qgYW#Y!ptQWo_~rf0uBHj^zqhZP?+WSiJuk=frze9F%evQyw#AGW5v_UZG*;=2Q8)EbpD?!V9^1l0Issks<2sk9g$N z`ODVL)Gj)tc_BdsZGEvo4i|2FV;zeD%c6!cuC-j%~sz-=d_&o-WLP zu|CPh#m~bbl=2-OIC5n>Zgg~g5w14-`(%3 zmL8zl*|Xhc?l|6$j(T!E@07O)H-xX|ZO;>2=35!7UnH7M1sbfldvLeDUcf{71(j`j zC1YZOuFj07ELTK+y|^i@`M`ho&*v#N-pIJfbK~Hfz?sddJ!*|}JmuD(EIw|6>O=1w zm$q&Zzb$5~Yo>j3jot3X(>6khJ@KBdC#yV06uY|HH+<8# zZ+cSmSNSZLLoU-J9&yi{xu@YY&lNrY7KyA^AmZq+AqT#YOnQv!5dV8Z#f^#*EqnUPx7>Z@6MI0xI(B=~Jp*N4!y}gK zWOxni6BC7x^;<_B4wUY29A7>5d7r-fe`hN;R*yKZ6|Et8%57KIq-u|4GpaeR9O$~V zY=hRFCeiFK({KA+DeO{RHM@Gvq$8uM_V6^wHuZz>4FMGA=3C)}{ zYL{D0_ITX%Hhw+ji>TDk)V-(5CLQY+UvpfI-=o@F;#R zbL&m-1A0s!9PWamZEZ(UzuoWruATjor;m3{vo$PPl{|CK^ty2y`KQ`0c=7qgubi|9 zzrOj>IzL@h&d0?p@3~VkO5P#=ru)TJn{Q@Ryh$%SC3WENgZVG+Z5G_u^Ww8_ScGYq zLCHtkPShXy&2{wY7m11+R&IHC$+HWu`m(|MdREQj&errV zHV16>mo~m?362O4Jz`^feLBa3?Q+y|ljWB09&X?4K68O~tn0-Cvz9#+{U|Z}vUdE2 zV<)&O3M$fj*N$kHtM&`wx#qa+^_{v{D`|hp^GQO>qeaT|Qm*x!-}7#M;@S%>fz#%M zZk0VJf7GTROz%bv=Q$anMVGAVmaQ7`fXD7a+=tUiJGIm=+wD7j_Eqc^p%tNxrf23F z>Q>)Dx*P6IIds1+Tejith#fLYrK0OAKmPLFYtr=b#zfI)#m>pcTvwd$s#{n#+bKRx z^tF}gav!A=J!4EPj&aY+9@QNt6Fes|BS+TG?wBI?C&TMmyuDi1IVHy0?pcd1Q=->& zrG%dT)SnJ_Xj;G&ixS^$@|Ot#HBIKd(K~3Q}DG3 zU48zW@iQ`OM=v}pcc#K;s(Qb(e?!`v3p);KXV~vdRBzm`UdpRjl-Efwb39Q0{QRYR&wUkAtTVOhf88roa2vNYxwy4Z zLm5qR84(zDb&o}Jf4lwjM5$)JS-+y%tY_4oa!8Y(~sgrefx3*2wvpdyW-n(ry-H`dbGI5X2)3cx6 zue(&YWTd{}Dd$ZOOD1iS-agmQwf~9!JPkL~sMf1XPM3Pk8#i8afnd+9!V=XsgSNct z8wPGaJi_Om3HYt?=3?%4{q0T;A0idM*m%WX81qePrFZGKfcfFJRhz75{HNP^yZ_pR zxm8vb|5>aIbi5)e9Cv*8nv==i$15k6CUW*vxt49=a8{e}`B9_J%-G`aaiTA0xpz3N z*!D@a$!)^b?Vt3&mUegPymwLJ-`qYS_HYZopAPp-D{eXOof^qAZr$q@6S=3n`K8LP zu)HPV8-1_s6@0IDLh!v`-^h|PPcFVy;8n=9NncfOrQ~#5Z%px(VijXu>2GcN3Cg9M z&FKbhN!L4+%28nN%d6ez_nIGA{L8-o4?lUNDZXIz)Oc8dyTO(5^o}eNi-QY6D|S;N z=yD}|dSoQ(sN{)AZ27W2ipSM_*{J`nPJMx%1T+a|^@%e-^LQSnXAmml3#TdqjYi8(n*>i+Z1Tb}FxwEXGoHK|rrIAzX> zjyRu45q1B0oqtx1+W6sGfx>a4zjP>K^a}xcA7xIdIbjP?D=_+$_0uCt+_)P!2J)5*ukQTULC9S zTZDsSi5dro9)WcnL1UB7QKAE9vxb{UwWX-bVFU-q=TUGYSMWG5ygBPSh=G6elNad^s42&< ziE4xR1W`v2#mkCyOWU@@T9|`l2L!whft5C*u_9vygyiVy=5RGvd6Zs4u_cVS$+8=u zvgemZDyQM;^sjzZLjLvClf~QqLG(Q5-I}szC(b-B$o)uxSDeYqg z1mY}34vw(|!rTi@7Uj1>9hXS;Q@$$AnhjmU6EYAf)x;UJR6eY6E{v8p4n+`7lo$M+ z`TgWLI0V58WU&7iKsWGYG9QKPFT$?h+o~Zf?kFW;izhgOxMLyKN%GwZOmgYJbQDbd zEn?cN)e!AhR5>`T2wip7(%^bahwmVhOmq+v-R;KQiV0t#%{PF1_WLcj$BEt!|~N^!{PBWVXdDK;2~(4x5#Kp6zk zNd%=)8H;t262Kt_i}?818h90zrsN1Py6$j3LDAg>t= zWTc&b-=krWcBD|lc}j>i3`_UOe-6+D6U_z_6Y?%;reP2g0{;+IWWKcXE2x@ILccaA zz!E=caEuR(^ZDhF6G}Syh1iUIVw!Y_DMSbUnjV>5J^7Hh8r(Yvg#r%(FR7!GSBIR7 z?X+ZO-R3fIj*}1+3j}!!KaGt!4X59B&f=G3<2}l?#~~ULLy*#NFTD|F;q|>s$%?TI zPf0b->T2j(5hV##(0+O5lR~KT1RsCaQA&)}w2s`_$0$&4G5n1o1e8>!q441VUgUZc zia8dNoW)hP&G7gcfRRIWs0t11yNWBQ%aj@`4Ebg_N<+{gND1BqTb?Qltf7q39faX{ zUDmoXaRV^j01uG)PDPuk9}gPL?ZilIe_M087+@RW&xWAl|_0Ktc z_@DwjNMdK{(pWH#vbp;>-@n%tr^X##v;pw@0Y8!8b~7N=l|j*EWR9Qi)2Gt|*jg|b znX%1`X{?VozYyBF6NZ)k8)VuR8gqZCC1CTRo00uH){Ln$4^naxkV49hWW?SNtxpjK zTOEWylJ}zpQ)gau{tTr$=pr2G(Mv6?e}Rf+Y-XNpNy8X{esTey2r@iPQZ_n1`GP+Z z!3PP26t~cdMO-N0*i*N_lda9>pk@LjA~FMJ+c3q|VJjC$D2+I=55zqWe`M0ta%55p zp^;VsqHO*0nTogo9x1rMtHTnC6OF~OZ>ixMw>%>3?|~7&{5(RSaS=2_mJ5~R=q6N7 z4s`)+Us<<|=btoSAoUzR5|_F#1(re`EBW}4;aSqYd@FZPw+1E-5ZD9$8T{oZ_bwb@ zE2nf&_<6F1O}W~;O#p;^27i`=82C3oIX#kw;pj)dPf{aBv!sR%$I__Lz}5*~!DomE zfBDIMh|_p*S~ZZ1cv0jiJ^?lpPuKt9e-@&_2fRv_j`L%g+6tlFJOUC(L49b9!O7xv zZ!BM-8wbbXoZ&U};-xIuq}spC>oylI{8Nhk0^HC9^Ov7I|1uLRjb|!fgpzDD4(w7WMuP6C#9e-=ZW~jjOmQrDX*$t%5}8#q|X=0wICo_YB)Brut6LOp`;UJz45*|{P78(`Urnyb`W{M zf|V~NJB;0yp6erl@&~wqq!egofdyOqWw4J?FS&p#NIB5bhtM*rg+)vIN^+EldvYdk zEWo7My0*e28m5A~_U%dh?0L-K`Dfl$fURU3VanTC#I2miC(YJDH&E>dd7yLlN(`^a zwsg{17@Z{8VDk$+xDP@9x(l@Ae5dgh4P%rIg_-=qY*@{z>;2k*wFN9`-P>=Nlmi1| zo~;0Tk^Ume!Z8o*#0fQ(Heq7tw{!#e8%h7DBImSj`#Kw#!=RgzCCKQHG#H9NY14mn z(AQtM7q*Vz;HXg;u6NUC8l75BX|qOs$C?_IjW8QM1PgcMnD~bSsZJOyI-%rY9}{QW z9j<=?mO985hjk;ESVqYsWcm+v9rwPe!&&JZG@(V zlo%5Z3#c{x{A_;4Yye~YhTMeL6n!LUY;Z8e9+8jsrLV$E)hghi*a0~HCqFrR0H!jR znuPAoAq&}RE03Zyat201_l zvHDYa-sm~P0EmFj7D)iYWLbDTHMm8P;~aAQ?yU({!9&YFvEkFxUvQfajs>uYDU$CP z8VGTU#w>$|gt^ba4;wN~ordKfpm^Er|H|S|MKL_w4S(cx=hhHR9$EGBvQL%1yZ1Z$ z0q1sb0vR@S8bg#Q^&l)_fb}1g7XHmoUZBOq8lj*=fSpxDi4hB{ zttmmPV!@3sp#gFzlNiGU^U}m`n|Vdh$~H2QmI+?)E{7P1honXh;cdDs4R{Jb{kmyC zIYQ06v+`Ri5F?P>jgb7MArq^P*6{wN;%GemqGuBbt_|E|k@eVwi5(JLhRt1_)rYx` z1L=GCBkPBYW-JYW;L<4O2pL#uizH^115OD9upPp z8-%jjKXRF#-HNCd&@LFXoI+@6;LhY7&#GnS9<3&4VEYW6m0atLpU6ZEgks%cGLSsl zLLD3dn*k$@3&E!6#l)(kz(2g~Bl5SuZ$wp!a z81>ZTsm!MbLT6PsuN2CcCEcHOc6>txIODY_2gf*qZs|0Z1{g74b%d-P@#7#V<2Q2P zUk=93PgW3(O%kGbbXj(!a7wZq*JGCe#_N9&Fqy5lKS-d7d>(U+_(zvL z+MN|q1WYB+fG1(fo|R0nJie)s+&089CUM>PD}K8ae>7@3f&H@@V~x>QZ%Ut4)J3oE z=2wPtaO}N4eAMDdV%miFFQVcw*|Tz)wWBZx$8Wje8ByRlHi`=JnL+U*dsQyJtcj#O z6UXM1;R5R+n24;B(mv8|{3XRH!`2<_L`E<1fO6nF^gA*aW`4!usvt*BPF}QsI;F^} zrN`F)1f~GnM_?msZ>!%7wv41#qfn0wHG(y`R­LCND8!PUWu8lm(W{Gys6O8w3$ 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 From c717d919af899755e593498efb797673f4816d54 Mon Sep 17 00:00:00 2001 From: F-Droid Translatebot Date: Mon, 7 Apr 2014 14:16:49 +0100 Subject: [PATCH 245/282] Translation updates --- res/values-fr/strings.xml | 28 +++++++++++++++ res/values-nl/strings.xml | 10 ++++++ res/values-tr/strings.xml | 76 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+) diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index a852ae8e7..9a3003151 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -18,6 +18,7 @@ Dernière analyse du dépôt : %s jamais Intervalle de mise à jour automatique + Pas de mise à jour automatique de la liste d\'applications Seulement par WiFi Mettre à jour automatiquement les listes d\'applications uniquement par WiFi Toujours mettre à jour automatiquement les listes d\'apps @@ -72,11 +73,14 @@ L\'URL d\'un dépôt ressemble à ceci : https://f-droid.org/repo Ce dépôt existe déja ! Ce dépôt est déjà configuré, cela va ajouter les informations des nouvelles clés. Ce dépôt est déja configuré, confirmer que vous voulez le réactiver. + Le dépôt entrant est déjà configuré et activé ! Vous devez d\'abord supprimer ce dépôt avant d\'en ajouter avec une clé différente. + URI de dépôt malformé ignoré : %s La liste des dépôts utilisés a changé. Voulez-vous les mettre à jour ? Mettre à jour les dépôts Gestion de dépôts + Bluetooth FDroid.apk… Préférences A propos Rechercher @@ -111,8 +115,11 @@ Voulez-vous les mettre à jour ? Rechercher des applications Compatibilité de l\'application Versions incompatibles + Afficher les versions des applications incompatibles avec l\'appareil Cacher les applications incompatibles avec l\'appareil Root + Ne pas griser les applications nécessitant les privilèges root + Griser les applications nécessitant les privilèges root Ignorer l\'écran tactile Toujours inclure les apps nécessitant un écran tactile Filtrer les apps normalement @@ -120,6 +127,7 @@ Voulez-vous les mettre à jour ? Quoi de neuf ? Mis à jour récemment Dépôt FDroid locaux + Découverte des dépôts FDroid locaux… Téléchargement %2$s / %3$s (%4$d%%) de %1$s @@ -134,6 +142,7 @@ Voulez-vous les mettre à jour ? Autorisations pour la version %s Afficher les autorisations Afficher la liste des permissions qu\'une app requiert + Ne pas afficher les permissions avant le téléchargement Vous n\'avez aucune application installée pour gérer %s Affichage compact Montrer les icônes à une taille plus petite @@ -142,14 +151,33 @@ Voulez-vous les mettre à jour ? Non signé URL Nombre d\'applications + Empreinte digitale de la clef de signature pour le dépôt (SHA-256) Description Dernière mise à jour Mise à jour Nom + Cela signifie que la liste + des applications n\'a pu être vérifiée. Vous + devriez faire attention avec les + applications téléchargées d\'indexes non signés. + Ce dépôt n\'a pas encore été utilisé. + Pour voir la liste des applications qu\'il + offre, vous devez le mettre à jour. + +Une fois la mise à jour effectuée, la description + et les autres détails seront affichés ici. Voulez vous supprimer le dépôt \"{0}\", qui a {1} apps ? Toutes les applications installées ne seront pas supprimés, mais vous ne serez plus en mesure de les mettre à jour via F-Droid. Inconnu Supprimer le dépôt ? Supprimer un dépôt signifie que les apps ne seront disponibles via F-Droid. Note: Toute les apps précédemment installées vont rester sur votre appareil. + « %1$s » a été désactivé.. + +Vous allez devoir + le réactiver pour installer des applications + à partir de ce dépôt. + %s ou plus récent jusqu\'à %s + De %1$s à %2$s + Votre appareil n\'est sur le même WiFi que le dépôt local que vous venez d\'ajouter ! Essayez de rejoindre ce réseau : %s Nécessite: %1$s diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index 08660f2a0..a4a4fa820 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -67,6 +67,7 @@ Wilt u ze vernieuwen? Zoeken Nieuwe bron Verwijder bron + Vind lokale oplagplaatsen Start Delen Installeren @@ -97,6 +98,7 @@ Wilt u ze vernieuwen? Alles Wat is nieuw Recentelijk vernieuwd + Lokale FDroid opslagplaatsen Downloaden %2$s / %3$s (%4$d%%) van %1$s @@ -116,5 +118,13 @@ Wilt u ze vernieuwen? Beschrijving Meest recente update Naam + Deze opslag is nog niet eerder gebruikt. Om de leverbare apps te zien moet u het updaten. + +Zodra de update gedaan is ziet u hier de beschrijving en andere details. + Weet u zeker dat u de opslag genaamd \"{0}\" met daarin \"{1}\" apps wilt verwijderen? Reeds geïnstalleerde apps blijven behouden, maar kunnen niet meer worden geüpdate via F-Droid. Onbekend + Het verwijderen van een opslag betekent dat apps hierop niet meer beschikbaar zijn voor F-Droid. + +Noot: Eerder geïnstalleerde apps blijven op uw apparaat. + \"%1$s\" uitgeschakeld. U moet deze weer activeren mocht u apps vanuit deze opslag willen installeren. diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml index 28aaf249d..4f706b50b 100644 --- a/res/values-tr/strings.xml +++ b/res/values-tr/strings.xml @@ -7,14 +7,26 @@ Bu paket cihazınızla uyumlu değil gibi görünüyor. Yine de kurmayı denemek istiyor musunuz? Bu uygulamanın eski bir sürümüne dönmek üzeresiniz. Bu, uygulamanın yanlış çalışmasına ve hatta veri kaybına neden olabilir. Devam etmek istiyor musunuz? Sürüm + Düzenle + Sil + NFC göndermesini etkinleştir İndirilen uygulamaları önbelleğe kaydet + İndirilen apk dosyalarını SD kartında tut + Apk dosyalarını saklama Güncellemeler Diğer Son depo analizi: %s asla + Otomatik güncelleme aralığı + Hiçbir otomatik uygulama listesi güncellemesi olmasın Sadece WiFi ile + Uygulama listesini otomatik olarak sadece wifi ile güncelle + Uygulama listesini daima otomatik olarak güncelle Bildirme + Güncellemeler bulunduğunda bunu bildir + Güncellemeleri hiçbir zaman bildirme Güncelleme tarihçesi + Uygulamaların yeni sayılacağı gün sayısı: %s Arama Sonuçları Uygulama Detayları Böyle bir uygulama bulunamadı @@ -39,6 +51,9 @@ Bir depo adresi şuna benzer: https://f-droid.org/repo Yeni depo ekle Ekle İptal + Etkinleştir + Anahtar ekle + Üzerine yaz Kaldırılacak depoyu seç Depoları güncelle Mevcut @@ -49,16 +64,29 @@ Bir depo adresi şuna benzer: https://f-droid.org/repo Bekleyiniz Uygulama listesi güncelleniyor… Uygulama buradan alınıyor: + NFC etkinleştirilmemiştir! + NFC ayarlarına git… + Hiçbir Bluetooth gönderme metodu bulunamadı, birisini seçin! + Bluetooth gönderme metodu seç Depo adresi + Parmak izi (seçime dayalı) + Bu depo zaten mevcut! + Bu depo zaten ayarlanmıştır, bu yeni anahtar verisi ekleyecektir. + Bu depo zaten ayarlanmıştır, tekrar etkinleştirmek istediğinizi teyit ediniz. + Gelen depo zaten kuruludur ve etkinleştirilmiştir! + Değişik anahtarla depo eklemeden evvel bu depoyu silmeniz gerekmektedir! + Yanlış yapılandırılmış depo URI\'si görmezden geliniyor: %s Kullanılan depoların listesi değişti. Güncellemek ister misiniz? Depoları güncelle Depoları Yönet + Bluetooth FDroid.apk… Tercihler Hakkında Arama Yeni Depo Depoyu kaldır + Yerel depoları bul Çalıştır Paylaş Kur @@ -79,16 +107,27 @@ Güncellemek ister misiniz? Bu uygulama özgür olmayan eklentiler tavsiye eder Bu uygulama özgür olmayan ağ servisleri tavsiye eder Bu uygulama özgür olmayan başka uygulamalara bağımlıdır + Kaynak kod tamamen özgür değildir Görüntüleme Uzman + Ek veri göster ve ilâve ayarları etkinleştir + Tecrübeli kullanıcılar için ilâveleri sakla Uygulama ara Uygulama uyumu Uyumsuz sürümler + Cihazla uyumsuz uygulama sürümlerini göster + Cihazla uyumsuz uygulama sürümlerini sakla Root + Root yetkileri gerektiren uygulamaları gri yapma + Root yetkileri gerektiren uygulamaları gri olarak göster Dokunmatik ekranı yok say + Dokunmatik ekran gerektiren uygulamaları daima dahil et + Uygulamaları normal olarak süz Tümü Yeni olanlar Yakın geçmişte güncellenen + Yerel FDroid depoları + Yerel FDroid depoları keşfediliyor… İndiriliyor %2$s / %3$s (%4$d%%) şuradan %1$s @@ -98,10 +137,47 @@ Güncellemek ister misiniz? %1$s konumuna bağlanılıyor Uygulamaların cihazınızla uyumluluğu kontrol ediliyor… + Uygulama detayları kaydediliyor (%1$d%%) Hiçbir izin kullanılmıyor. %s sürümü için izinler İzinleri göster + Uygulamaların gerektirdiği izinlerin listesini göster + İndirmeden önce izinleri gösterme %s unsurunu yönetecek hiçbir mevcut uygulamanız yok Yoğun düzen + İkonları daha küçük boyutta görüntüle + İkonları normal boyutta görüntüle Tema + İmzasız + URL + Uygulama sayısı + Depo imza anahtarının parmak izi (SHA-256) + Betimleme + Son güncelleme + Güncelle + İsim + Bu, uygulama listesinin + teyit edilemediği anlamına gelir. İmzasız + endekslerden indirilen uygulamaları dikkatle + kullanmanız gerekir. + Bu depo henüz kullanılmamıştır. + Sunduğu uygulamaları görmek için onu güncellemeniz gerekir. + +Güncelleme yapıldığında betimleler ve diğer ayrıntılar burada gösterilecektir. + {1} uygulama içeren \"{0}\" deposunu silmek istiyor musunuz? Kurulu uygulamalar KALDIRILMAYACAKTIR, ancak bundan böyle onları F-Droid vasıtasıyla güncellemek mümkün olmayacaktır. + Bilinmiyor + Depo silinsin mi? + Bir depoyu silmek, ondan indirilen uygulamaların + bundan böyle F-Droid vasıtasıyla elde edilemeyeceği anlamına gelir. + +Not: Tüm + önceden kurulan uygulamalar cihazınızda kalacaktır. + \"%1$s\" devre dışı bırakıldı. + +Ondan uygulama indirmek için bu depoyu tekrar etkinleştirmeniz gerekecektir. + %s ya da sonrası + %s değerine kadar + %1$s değerinden %2$s değerine kadar + Cihazınız eklemiş olduğunuz yerel depoyla aynı WiFi\'de değildir. Şu şebekeye katılmayı deneyin: %s + Gerektirdiği: %1$s From ad9218d14c2dfed2b6aaa2c4c3b78be9e17318f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 7 Apr 2014 15:21:23 +0200 Subject: [PATCH 246/282] Fix some string formats and an ellipsis --- res/values-es/strings.xml | 2 +- res/values-fa/strings.xml | 2 +- res/values-pl/strings.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 68486ad17..b1afbb304 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -176,7 +176,7 @@ Nota: todas las aplicaciones previamente instaladas se quedarán en tu dispositi Necesitarás volver a habilitar este repositorio para instalar aplicaciones desde él. %s o posterior hasta %s - De %1$ a %2$s + De %1$s a %2$s ¡Tu dispositivo no está en la misma WiFi que el repo que acabas de añadir! Intenta unirte a esta red: %s Requiere: %1$s diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml index 9d0de06d1..243faedf6 100644 --- a/res/values-fa/strings.xml +++ b/res/values-fa/strings.xml @@ -9,7 +9,7 @@ نسخه ویرایش حذف - فعال‌سازی ارسال ان‌اف‌سی... + فعال‌سازی ارسال ان‌اف‌سی… میانگیری برنامه‌های دریافت‌شده نگه‌داشتن پرونده‌های ای‌پی‌کی بر روی کارت اس‌دی هیچ پروندهٔ ای‌پی‌کی را نگه‌ندار diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index fc1cc4a16..6e709bf01 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -150,7 +150,7 @@ Opis i inne szczegóły będą tu dostępne gdy zaktualizujesz repozytoriumUsunięcie repozytorium oznacza, że aplikacje z niego nie będą dłużej dostępne w F-Droid. Uwaga: Wszystkie poprzednio zainstalowane aplikacje zostaną na urządzeniu. - Zablokowane \"%1%s\". + Zablokowane \"%1$s\". Aby zainstalować aplikacje z tego repozytorium musisz je włączyć ponownie. %s lub później From b731ab57b3c73b0c5701a4386d25dca1c946142c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 7 Apr 2014 15:46:33 +0200 Subject: [PATCH 247/282] Release 0.63 --- AndroidManifest.xml | 4 ++-- CHANGELOG.md | 2 +- res/values/no_trans.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4829a4fb6..4d3db9f80 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="630" + android:versionName="0.63" > F-Droid - 0.62 + 0.63 https://f-droid.org team@f-droid.org From a477f421cb781554ae119565ff4de21e43eed1e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 7 Apr 2014 19:35:04 +0200 Subject: [PATCH 248/282] Greatly improve app list layout * Don't hard-code ellipsis in the code * Separate the two rows into two linear layouts * Don't abuse relative layouts * Use ellipsize with weights to achieve best results --- res/layout/applistitem.xml | 116 ++++++++++-------- .../fdroid/fdroid/views/AppListAdapter.java | 14 +-- 2 files changed, 67 insertions(+), 63 deletions(-) diff --git a/res/layout/applistitem.xml b/res/layout/applistitem.xml index e0ff94d1d..e93f27d2d 100644 --- a/res/layout/applistitem.xml +++ b/res/layout/applistitem.xml @@ -14,74 +14,86 @@ android:layout_width="56dp" android:layout_height="56dp" android:layout_centerVertical="true" - android:padding="4dp" + android:padding="5dp" android:scaleType="fitCenter" /> - - + android:baselineAligned="false" + > - + + + + + + + android:baselineAligned="false" + > - + - + + + - + + + diff --git a/src/org/fdroid/fdroid/views/AppListAdapter.java b/src/org/fdroid/fdroid/views/AppListAdapter.java index fb9003586..03361fcf0 100644 --- a/src/org/fdroid/fdroid/views/AppListAdapter.java +++ b/src/org/fdroid/fdroid/views/AppListAdapter.java @@ -120,13 +120,6 @@ abstract public class AppListAdapter extends CursorAdapter { } } - private String ellipsize(String input, int maxLength) { - if (input == null || input.length() < maxLength+1) { - return input; - } - return input.substring(0, maxLength) + "…"; - } - private String getVersionInfo(App app) { if (app.suggestedVercode <= 0) { @@ -136,19 +129,18 @@ abstract public class AppListAdapter extends CursorAdapter { PackageInfo installedInfo = app.getInstalledInfo(mContext); if (installedInfo == null) { - return ellipsize(app.getSuggestedVersion(), 12); + return app.getSuggestedVersion(); } String installedVersionString = installedInfo.versionName; int installedVersionCode = installedInfo.versionCode; if (app.canAndWantToUpdate(mContext) && showStatusUpdate()) { - return ellipsize(installedVersionString, 8) + - " → " + ellipsize(app.getSuggestedVersion(), 8); + return installedVersionString + " → " + app.getSuggestedVersion(); } if (installedVersionCode > 0 && showStatusInstalled()) { - return ellipsize(installedVersionString, 12) + " ✔"; + return installedVersionString + " ✔"; } return installedVersionString; From 407e7662e959f736c992f921b7fd98fadbf10d6b Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 7 Apr 2014 21:07:01 -0400 Subject: [PATCH 249/282] support QR scanners that do not respect custom URI schemes Some QR Code scanners don't respect custom schemes like fdroidrepo://, so this is a workaround, since the local repo URI is all uppercase in the QR Code for sending the local repo to another device. This way, the QR Code can still be all uppercase and use HTTP:// and Android will still route it to FDroid, but via the Just Once/Always chooser (fdroidrepo:// goes directly to FDroid with no prompt, when it works) --- AndroidManifest.xml | 6 ++++++ .../fdroid/fdroid/views/fragments/RepoListFragment.java | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4d3db9f80..cafacbd19 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -129,6 +129,12 @@ + + diff --git a/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java index c30f23397..fa43cb82b 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java @@ -208,6 +208,14 @@ public class RepoListFragment extends ListFragment * case means it should be downcased. */ uri = Uri.parse(uri.toString().toLowerCase(Locale.ENGLISH)); + } else if (uri.getPath().startsWith("/FDROID/REPO")) { + /* + * some QR scanners chop off the fdroidrepo:// and just try + * http://, then the incoming URI does not get downcased + * properly, and the query string is stripped off. So just + * downcase the path, and carry on to get something working. + */ + uri = Uri.parse(uri.toString().toLowerCase(Locale.ENGLISH)); } // make scheme and host lowercase so they're readable in dialogs scheme = scheme.toLowerCase(Locale.ENGLISH); From c1d0ec43c3e66a32cf71d95bcdfda79ee7ed585f Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 7 Apr 2014 21:39:50 -0400 Subject: [PATCH 250/282] fix crasher when hopping around apps and adding repos I triggered this a few times while trying various QR Code scanning apps with FDroid. fixes #3222 https://dev.guardianproject.info/issues/3222 --- src/org/fdroid/fdroid/FDroid.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index 6576b8582..08b6e3609 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -257,7 +257,7 @@ public class FDroid extends FragmentActivity { case REQUEST_APPDETAILS: break; case REQUEST_MANAGEREPOS: - if (data.hasExtra(ManageRepo.REQUEST_UPDATE)) { + if (data != null && 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); From da329089fb563df146a409b1850bd89e1f772f0e Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 8 Apr 2014 00:16:03 -0400 Subject: [PATCH 251/282] add custom ant rules for downloading JUnit results --- custom_rules.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 custom_rules.xml diff --git a/custom_rules.xml b/custom_rules.xml new file mode 100644 index 000000000..8306b3fb5 --- /dev/null +++ b/custom_rules.xml @@ -0,0 +1,15 @@ + + + + + Downloading XML test report… + + + + + + + + + + From 87638363b33d8f9f886b070609d9d77bf1e44e5a Mon Sep 17 00:00:00 2001 From: James Clark Date: Thu, 10 Apr 2014 03:45:17 +0100 Subject: [PATCH 252/282] Set listview to top item after category refresh --- src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java index 98f70d35a..02cd9dd90 100644 --- a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java @@ -156,6 +156,7 @@ public class AvailableAppsFragment extends AppListFragment implements currentCategory = category; Log.d("FDroid", "Category '" + currentCategory + "' selected."); getLoaderManager().restartLoader(0, null, AvailableAppsFragment.this); + getListView().setSelection(0); } @Override From 09ccf3d428e87cba621264870d2e2e37b5b08129 Mon Sep 17 00:00:00 2001 From: James Clark Date: Thu, 10 Apr 2014 04:14:55 +0100 Subject: [PATCH 253/282] Fix bug introduced in last commit --- .../fdroid/fdroid/views/fragments/AvailableAppsFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java index 02cd9dd90..c3d1206ab 100644 --- a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java @@ -103,6 +103,7 @@ public class AvailableAppsFragment extends AppListFragment implements categorySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int pos, long id) { + getListView().setSelection(0); setCurrentCategory(categories.get(pos)); } @Override @@ -156,7 +157,6 @@ public class AvailableAppsFragment extends AppListFragment implements currentCategory = category; Log.d("FDroid", "Category '" + currentCategory + "' selected."); getLoaderManager().restartLoader(0, null, AvailableAppsFragment.this); - getListView().setSelection(0); } @Override From f04ac83ec9d5cef408794b962c61e3e6ed67c640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 10 Apr 2014 15:08:29 +0200 Subject: [PATCH 254/282] Fix build without java 1.7 compatibility --- .../fdroid/fdroid/views/fragments/AvailableAppsFragment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java index 3037352b5..29c3a2295 100644 --- a/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AvailableAppsFragment.java @@ -89,9 +89,9 @@ public class AvailableAppsFragment extends AppListFragment implements final List categories = AppProvider.Helper.categories(getActivity()); // attempt to translate category names with fallback to default name - List translatedCategories = new ArrayList<>(categories.size()); + List translatedCategories = new ArrayList(categories.size()); Resources res = getResources(); - for (String category:categories) + for (String category : categories) { int id = res.getIdentifier(category.replace(" & ", "_"), "string", getActivity().getPackageName()); translatedCategories.add(id == 0 ? category : getString(id)); From 40d873551fb4aa6f118e727d54d6ed064ebae368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 10 Apr 2014 15:11:00 +0200 Subject: [PATCH 255/282] A bit of extra cursor safety, avoid the last missing .close() --- src/org/fdroid/fdroid/data/RepoProvider.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/org/fdroid/fdroid/data/RepoProvider.java b/src/org/fdroid/fdroid/data/RepoProvider.java index 148ebfb09..78c9329db 100644 --- a/src/org/fdroid/fdroid/data/RepoProvider.java +++ b/src/org/fdroid/fdroid/data/RepoProvider.java @@ -31,8 +31,10 @@ public class RepoProvider extends FDroidProvider { Cursor cursor = resolver.query(uri, projection, null, null, null); Repo repo = null; if (cursor != null) { - cursor.moveToFirst(); - repo = new Repo(cursor); + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + repo = new Repo(cursor); + } cursor.close(); } return repo; @@ -73,12 +75,15 @@ public class RepoProvider extends FDroidProvider { } private static List cursorToList(Cursor cursor) { - List repos = new ArrayList(); + int knownRepoCount = cursor != null ? cursor.getCount() : 0; + List repos = new ArrayList(knownRepoCount); if (cursor != null) { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - repos.add(new Repo(cursor)); - cursor.moveToNext(); + if (knownRepoCount > 0) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + repos.add(new Repo(cursor)); + cursor.moveToNext(); + } } cursor.close(); } From 2cdb634865e34def85648480be20ede4061a2eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 10 Apr 2014 15:23:19 +0200 Subject: [PATCH 256/282] Fixes #6: Spaces before ellipsis in German are OK --- res/values-de/strings.xml | 10 +++++----- tools/fix-ellipsis.sh | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 0636ae0c8..f324dd74f 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -9,7 +9,7 @@ Version Bearbeiten Löschen - NFC-Transfer aktivieren… + NFC-Transfer aktivieren … Anwendungszwischenspeicher Heruntergeladene Programmpakete auf der SD-Karte behalten Installationsdateien nicht behalten @@ -66,7 +66,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Anwendungsliste wird aktualisiert… Anwendung wird heruntergeladen von NFC ist deaktiviert! - NFC-Einstellungen öffnen… + NFC-Einstellungen öffnen … Keine Bluetooth-Sendemethode gefunden. Bitte wählen Sie eine aus! Bluetooth-Sendemethode auswählen Adresse der Paketquelle @@ -81,7 +81,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re Sollen diese aktualisiert werden? Paketquellen aktualisieren Paketquellen verwalten - Bluetooth-FDroid.apk… + Bluetooth-FDroid.apk … Einstellungen Über Suchen @@ -128,7 +128,7 @@ Sollen diese aktualisiert werden? Was gibt es Neues Kürzlich Aktualisiert Lokale F-Droid-Paketquellen - Lokale F-Droid-Paketquellen entdecken… + Lokale F-Droid-Paketquellen entdecken … Herunterladen %2$s / %3$s (%4$d%%) von %1$s @@ -137,7 +137,7 @@ Sollen diese aktualisiert werden? %1$s Verbinden mit %1$s - Kompatibilität mit Ihrem Gerät wird überprüft… + Kompatibilität mit Ihrem Gerät wird überprüft … App-Details speichern (%1$d%%) Es werden keine Berechtigungen verwendet. Berechtigungen für Version %s diff --git a/tools/fix-ellipsis.sh b/tools/fix-ellipsis.sh index 490027b75..b3f02fb85 100755 --- a/tools/fix-ellipsis.sh +++ b/tools/fix-ellipsis.sh @@ -2,4 +2,4 @@ # Fix TypographyEllipsis programmatically -sed -i -e 's/\.\.\./…/g' -e 's/ …/…/g' res/values*/*.xml +sed -i 's/\.\.\./…/g' res/values*/*.xml From 7fd3ea236e62847ae3a7e48ce8ce18a81ab16b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 10 Apr 2014 15:49:02 +0200 Subject: [PATCH 257/282] Move "appid" into AppDetails.EXTRA_APPID --- src/org/fdroid/fdroid/AppDetails.java | 7 +++++-- src/org/fdroid/fdroid/FDroid.java | 2 +- src/org/fdroid/fdroid/SearchResults.java | 2 +- src/org/fdroid/fdroid/views/fragments/AppListFragment.java | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index ba1ff18e4..4cf2b64e6 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -74,6 +74,9 @@ public class AppDetails extends ListActivity { private static final int REQUEST_INSTALL = 0; private static final int REQUEST_UNINSTALL = 1; + + public static final String EXTRA_APPID = "appid"; + private ApkListAdapter adapter; private static class ViewHolder { @@ -287,10 +290,10 @@ public class AppDetails extends ListActivity { } Log.d("FDroid", "AppDetails launched from link, for '" + appid + "'"); - } else if (!i.hasExtra("appid")) { + } else if (!i.hasExtra(EXTRA_APPID)) { Log.d("FDroid", "No application ID in AppDetails!?"); } else { - appid = i.getStringExtra("appid"); + appid = i.getStringExtra(EXTRA_APPID); } if (i.hasExtra("from")) { diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index 08b6e3609..8dc53721d 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -106,7 +106,7 @@ public class FDroid extends FragmentActivity { } if (appid != null && appid.length() > 0) { Intent call = new Intent(this, AppDetails.class); - call.putExtra("appid", appid); + call.putExtra(AppDetails.EXTRA_APPID, appid); startActivityForResult(call, REQUEST_APPDETAILS); } diff --git a/src/org/fdroid/fdroid/SearchResults.java b/src/org/fdroid/fdroid/SearchResults.java index 01adb01b1..8f62100ba 100644 --- a/src/org/fdroid/fdroid/SearchResults.java +++ b/src/org/fdroid/fdroid/SearchResults.java @@ -138,7 +138,7 @@ public class SearchResults extends ListActivity { app = new App((Cursor) adapter.getItem(position)); Intent intent = new Intent(this, AppDetails.class); - intent.putExtra("appid", app.id); + intent.putExtra(AppDetails.EXTRA_APPID, app.id); startActivityForResult(intent, REQUEST_APPDETAILS); super.onListItemClick(l, v, position, id); } diff --git a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java index c6c674575..7e6860a43 100644 --- a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java @@ -118,7 +118,7 @@ abstract public class AppListFragment extends ListFragment implements public void onItemClick(AdapterView parent, View view, int position, long id) { final App app = new App((Cursor)getListView().getItemAtPosition(position)); Intent intent = new Intent(getActivity(), AppDetails.class); - intent.putExtra("appid", app.id); + intent.putExtra(AppDetails.EXTRA_APPID, app.id); intent.putExtra("from", getFromTitle()); startActivityForResult(intent, FDroid.REQUEST_APPDETAILS); } From f6707490f8a6fb03b153d5ce2721c1e7c6c2bfea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 10 Apr 2014 15:52:11 +0200 Subject: [PATCH 258/282] Move "from" into AppDetails.EXTRA_FROM --- src/org/fdroid/fdroid/AppDetails.java | 8 ++++---- .../fdroid/fdroid/views/fragments/AppListFragment.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 4cf2b64e6..7364e85bf 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -76,6 +76,7 @@ public class AppDetails extends ListActivity { private static final int REQUEST_UNINSTALL = 1; public static final String EXTRA_APPID = "appid"; + public static final String EXTRA_FROM = "from"; private ApkListAdapter adapter; @@ -288,16 +289,15 @@ public class AppDetails extends ListActivity { // fdroid.app:app.id appid = data.getEncodedSchemeSpecificPart(); } - Log.d("FDroid", "AppDetails launched from link, for '" + appid - + "'"); + Log.d("FDroid", "AppDetails launched from link, for '" + appid + "'"); } else if (!i.hasExtra(EXTRA_APPID)) { Log.d("FDroid", "No application ID in AppDetails!?"); } else { appid = i.getStringExtra(EXTRA_APPID); } - if (i.hasExtra("from")) { - setTitle(i.getStringExtra("from")); + if (i.hasExtra(EXTRA_FROM)) { + setTitle(i.getStringExtra(EXTRA_FROM)); } mPm = getPackageManager(); diff --git a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java index 7e6860a43..f2cfc9f93 100644 --- a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java @@ -119,7 +119,7 @@ abstract public class AppListFragment extends ListFragment implements final App app = new App((Cursor)getListView().getItemAtPosition(position)); Intent intent = new Intent(getActivity(), AppDetails.class); intent.putExtra(AppDetails.EXTRA_APPID, app.id); - intent.putExtra("from", getFromTitle()); + intent.putExtra(AppDetails.EXTRA_FROM, getFromTitle()); startActivityForResult(intent, FDroid.REQUEST_APPDETAILS); } From e7f76705c84416ccc3dfe7e9961f48abdaa6d47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 10 Apr 2014 15:55:24 +0200 Subject: [PATCH 259/282] Move "receiver" and "address" into UpdateService.EXTRA_... --- src/org/fdroid/fdroid/UpdateService.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index 201ab8d79..35b603aad 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -48,6 +48,9 @@ public class UpdateService extends IntentService implements ProgressListener { public static final int STATUS_ERROR = 2; public static final int STATUS_INFO = 3; + public static final String EXTRA_RECEIVER = "receiver"; + public static final String EXTRA_ADDRESS = "address"; + private ResultReceiver receiver = null; public UpdateService() { @@ -136,9 +139,10 @@ public class UpdateService extends IntentService implements ProgressListener { Intent intent = new Intent(context, UpdateService.class); UpdateReceiver receiver = new UpdateReceiver(new Handler()); receiver.setContext(context).setDialog(dialog); - intent.putExtra("receiver", receiver); - if (!TextUtils.isEmpty(address)) - intent.putExtra("address", address); + intent.putExtra(EXTRA_RECEIVER, receiver); + if (!TextUtils.isEmpty(address)) { + intent.putExtra(EXTRA_ADDRESS, address); + } context.startService(intent); return receiver; @@ -242,8 +246,8 @@ public class UpdateService extends IntentService implements ProgressListener { @Override protected void onHandleIntent(Intent intent) { - receiver = intent.getParcelableExtra("receiver"); - String address = intent.getStringExtra("address"); + receiver = intent.getParcelableExtra(EXTRA_RECEIVER); + String address = intent.getStringExtra(EXTRA_ADDRESS); long startTime = System.currentTimeMillis(); String errmsg = ""; From 8ed76f47eef59fa182e253bdf545b3d7c0f4f001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 10 Apr 2014 16:33:15 +0200 Subject: [PATCH 260/282] More improvements to the app list layout * Don't use a RelativeLayout for the whole thing * Use more external paddings, not per-element paddings * Center everything vertically --- res/layout/applistitem.xml | 28 ++++++++++--------- .../fdroid/fdroid/views/AppListAdapter.java | 6 ++-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/res/layout/applistitem.xml b/res/layout/applistitem.xml index e93f27d2d..736f947b4 100644 --- a/res/layout/applistitem.xml +++ b/res/layout/applistitem.xml @@ -1,9 +1,9 @@ - - - + diff --git a/src/org/fdroid/fdroid/views/AppListAdapter.java b/src/org/fdroid/fdroid/views/AppListAdapter.java index 03361fcf0..d7c179c49 100644 --- a/src/org/fdroid/fdroid/views/AppListAdapter.java +++ b/src/org/fdroid/fdroid/views/AppListAdapter.java @@ -9,7 +9,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import android.widget.RelativeLayout; +import android.widget.LinearLayout; import android.widget.TextView; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; @@ -151,8 +151,8 @@ abstract public class AppListAdapter extends CursorAdapter { ? R.dimen.applist_icon_compact_size : R.dimen.applist_icon_normal_size)); - RelativeLayout.LayoutParams params = - (RelativeLayout.LayoutParams)icon.getLayoutParams(); + LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams)icon.getLayoutParams(); params.height = size; params.width = size; From 4db53deb420c7393f985bc1cb0cb0781adf35e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 11 Apr 2014 19:24:46 +0200 Subject: [PATCH 261/282] Forgot to set the icon sizes back to normal They got added +8 since we added paddings directly to the icon layout. Since those paddings got removed, this has to be switched back too. --- res/layout/applistitem.xml | 4 ++-- res/values/dimen.xml | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/res/layout/applistitem.xml b/res/layout/applistitem.xml index 736f947b4..2956c0b2e 100644 --- a/res/layout/applistitem.xml +++ b/res/layout/applistitem.xml @@ -11,8 +11,8 @@ diff --git a/res/values/dimen.xml b/res/values/dimen.xml index 14b879442..f20a66205 100644 --- a/res/values/dimen.xml +++ b/res/values/dimen.xml @@ -1,7 +1,5 @@ - - 56dp - - 40dp + 48dp + 32dp From bc6f3d5cd9b79970472f067d5f810c431cf02101 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 8 Apr 2014 10:07:37 -0400 Subject: [PATCH 262/282] add `ant javadoc` to generate javadoc for FDroid sources --- custom_rules.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/custom_rules.xml b/custom_rules.xml index 8306b3fb5..944377a7c 100644 --- a/custom_rules.xml +++ b/custom_rules.xml @@ -12,4 +12,12 @@ + + + + From d813f1ec173e71aec5549b3ed772a1cb0971e224 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 10 Apr 2014 13:05:59 -0400 Subject: [PATCH 263/282] run JUnit tests using android-junit-report to get XML output Jenkins needs some kind of report from the JUnit tests in order to tell whether the tests succeeded or not. android-junit-report is a library to do exactly that. With this setup, Jenkins should now successfully understand the status of the JUnit tests, where before it just ran them and ignored the results --- custom_rules.xml | 13 +------------ test/.gitignore | 1 + test/AndroidManifest.xml | 2 +- test/ant.properties | 1 + test/custom_rules.xml | 19 +++++++++++++++++++ test/libs/android-junit-report-1.5.8.README | 5 +++++ test/libs/android-junit-report-1.5.8.jar | Bin 0 -> 9202 bytes 7 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 test/custom_rules.xml create mode 100644 test/libs/android-junit-report-1.5.8.README create mode 100644 test/libs/android-junit-report-1.5.8.jar diff --git a/custom_rules.xml b/custom_rules.xml index 944377a7c..7747834aa 100644 --- a/custom_rules.xml +++ b/custom_rules.xml @@ -1,16 +1,5 @@ - - - - Downloading XML test report… - - - - - - - - + - diff --git a/test/ant.properties b/test/ant.properties index 16244024c..99458bdd5 100644 --- a/test/ant.properties +++ b/test/ant.properties @@ -16,3 +16,4 @@ # The password will be asked during the build when you use the 'release' target. tested.project.dir=.. +test.runner=com.zutubi.android.junitreport.JUnitReportTestRunner diff --git a/test/custom_rules.xml b/test/custom_rules.xml new file mode 100644 index 000000000..ee90d3fac --- /dev/null +++ b/test/custom_rules.xml @@ -0,0 +1,19 @@ + + + + + + Downloading XML test report (/data/data/${tested.package}/files/junit-report.xml)… + + + + + + + + + + diff --git a/test/libs/android-junit-report-1.5.8.README b/test/libs/android-junit-report-1.5.8.README new file mode 100644 index 000000000..a89c98533 --- /dev/null +++ b/test/libs/android-junit-report-1.5.8.README @@ -0,0 +1,5 @@ + +Needed for Jenkins to get JUnit reports. + +* https://github.com/jsankey/android-junit-report +* https://github.com/downloads/jsankey/android-junit-report/android-junit-report-1.5.8.jar diff --git a/test/libs/android-junit-report-1.5.8.jar b/test/libs/android-junit-report-1.5.8.jar new file mode 100644 index 0000000000000000000000000000000000000000..09e6a2d4f5ffab82e583fe6fc7b327ce6626e95c GIT binary patch literal 9202 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 Bv5Wu! literal 0 HcmV?d00001 From 8c6ce67100fdcbce4f9a6162f2b08737c7fa52ba Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Sat, 12 Apr 2014 09:25:26 +0000 Subject: [PATCH 264/282] Added test for "ApkProvider.delete(..., List)" This was explicitly not-allowed previously, and so there was a test that ensured it threw an exception when attempted on the ApkProvider. However I implemented it for another feature, but forgot to change the tests. Now the test no longer tests for an exception. Rather, it properly tests for the correct execution of the method. --- .../org/fdroid/fdroid/ApkProviderTest.java | 56 +++++++++++++++++-- .../fdroid/fdroid/BaseApkProviderTest.java | 6 +- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/test/src/org/fdroid/fdroid/ApkProviderTest.java b/test/src/org/fdroid/fdroid/ApkProviderTest.java index 221d60aa1..e77ef41bc 100644 --- a/test/src/org/fdroid/fdroid/ApkProviderTest.java +++ b/test/src/org/fdroid/fdroid/ApkProviderTest.java @@ -6,6 +6,7 @@ import android.net.Uri; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; +import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.mock.MockApk; import org.fdroid.fdroid.mock.MockApp; @@ -102,14 +103,61 @@ public class ApkProviderTest extends BaseApkProviderTest { // which are tested elsewhere. } + public void testDeleteArbitraryApks() { + Apk one = insertApkForRepo("com.example.one", 1, 10); + Apk two = insertApkForRepo("com.example.two", 1, 10); + Apk three = insertApkForRepo("com.example.three", 1, 10); + Apk four = insertApkForRepo("com.example.four", 1, 10); + Apk five = insertApkForRepo("com.example.five", 1, 10); + + assertTotalApkCount(5); + + assertEquals("com.example.one", one.id); + assertEquals("com.example.two", two.id); + assertEquals("com.example.five", five.id); + + String[] expectedIds = { + "com.example.one", + "com.example.two", + "com.example.three", + "com.example.four", + "com.example.five", + }; + + List all = ApkProvider.Helper.findByRepo(getSwappableContext(), new MockRepo(10), ApkProvider.DataColumns.ALL); + List actualIds = new ArrayList(); + for (Apk apk : all) { + actualIds.add(apk.id); + } + + TestUtils.assertContainsOnly(actualIds, expectedIds); + + List toDelete = new ArrayList(3); + toDelete.add(two); + toDelete.add(three); + toDelete.add(four); + ApkProvider.Helper.deleteApks(getSwappableContext(), toDelete); + + assertTotalApkCount(2); + + List allRemaining = ApkProvider.Helper.findByRepo(getSwappableContext(), new MockRepo(10), ApkProvider.DataColumns.ALL); + List actualRemainingIds = new ArrayList(); + for (Apk apk : allRemaining) { + actualRemainingIds.add(apk.id); + } + + String[] expectedRemainingIds = { + "com.example.one", + "com.example.five", + }; + + TestUtils.assertContainsOnly(actualRemainingIds, expectedRemainingIds); + } + public void testInvalidDeleteUris() { Apk apk = new MockApk("org.fdroid.fdroid", 10); - List apks = new ArrayList(); - apks.add(apk); - assertCantDelete(ApkProvider.getContentUri()); - assertCantDelete(ApkProvider.getContentUri(apks)); assertCantDelete(ApkProvider.getContentUri("org.fdroid.fdroid", 10)); assertCantDelete(ApkProvider.getContentUri(apk)); assertCantDelete(Uri.withAppendedPath(ApkProvider.getContentUri(), "some-random-path")); diff --git a/test/src/org/fdroid/fdroid/BaseApkProviderTest.java b/test/src/org/fdroid/fdroid/BaseApkProviderTest.java index e42818232..7c2e42bc4 100644 --- a/test/src/org/fdroid/fdroid/BaseApkProviderTest.java +++ b/test/src/org/fdroid/fdroid/BaseApkProviderTest.java @@ -2,6 +2,7 @@ package org.fdroid.fdroid; import android.content.ContentValues; import android.database.Cursor; +import android.net.Uri; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; @@ -68,9 +69,10 @@ abstract class BaseApkProviderTest extends FDroidProviderTest { } } - protected void insertApkForRepo(String id, int versionCode, long repoId) { + protected Apk insertApkForRepo(String id, int versionCode, long repoId) { ContentValues additionalValues = new ContentValues(); additionalValues.put(ApkProvider.DataColumns.REPO_ID, repoId); - TestUtils.insertApk(this, id, versionCode, additionalValues); + Uri uri = TestUtils.insertApk(this, id, versionCode, additionalValues); + return ApkProvider.Helper.get(getSwappableContext(), uri); } } From de085f7e0297693d3932ad1f98edd4e6640d4177 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Sat, 12 Apr 2014 19:02:15 +0000 Subject: [PATCH 265/282] Added ApkProvider.get() to return a single apk. This allows you to specify the Uri of a single apk, and it will return it. Right now it is just used in a test, but hopefully it will be useful in other situations too. I forgot to commit this last time, and didn't review my patch well enough before submitting. --- src/org/fdroid/fdroid/data/ApkProvider.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java index 942af616b..9f4f7630b 100644 --- a/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -116,6 +116,24 @@ public class ApkProvider extends FDroidProvider { Cursor cursor = resolver.query(uri, fields, null, null, null); return cursorToList(cursor); } + + public static Apk get(Context context, Uri uri ) { + return get(context, uri, DataColumns.ALL); + } + + public static Apk get(Context context, Uri uri, String[] fields) { + ContentResolver resolver = context.getContentResolver(); + Cursor cursor = resolver.query(uri, fields, null, null, null); + Apk apk = null; + if (cursor != null) { + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + apk = new Apk(cursor); + } + cursor.close(); + } + return apk; + } } public interface DataColumns extends BaseColumns { From 57eaad7c1b0e0b4426cf6785fdab50991771d2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 17 Apr 2014 01:20:32 +0200 Subject: [PATCH 266/282] Remove RelativeLayout leftovers --- res/layout/applistitem.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/layout/applistitem.xml b/res/layout/applistitem.xml index 2956c0b2e..449d974bd 100644 --- a/res/layout/applistitem.xml +++ b/res/layout/applistitem.xml @@ -23,8 +23,6 @@ android:layout_height="wrap_content" android:paddingLeft="10dp" android:paddingStart="10dp" - android:layout_toRightOf="@id/icon" - android:layout_toEndOf="@id/icon" android:layout_gravity="center_vertical" android:baselineAligned="false" > From 655f2bf7e3243d4f2608cdc74ee963af1959e552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 17 Apr 2014 01:20:42 +0200 Subject: [PATCH 267/282] Update UIL --- extern/UniversalImageLoader | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/UniversalImageLoader b/extern/UniversalImageLoader index b1b49e51f..29811229c 160000 --- a/extern/UniversalImageLoader +++ b/extern/UniversalImageLoader @@ -1 +1 @@ -Subproject commit b1b49e51f2c43b119edca44691daf9ab6c751158 +Subproject commit 29811229c3ba3da390b29353875be2c92f88a789 From 4e24050760f6295e11020813cf9ecc17319c44cb Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Mon, 14 Apr 2014 00:08:31 +1000 Subject: [PATCH 268/282] Adding our own cache of currently installed apks in the database. Previously the data was not stored anywhere, and each time we wanted to know about all installed apps, we built a ridiculously long SQL query. The query had essentially one "OR" clause for each installed app. To make matters worse, it also required one parameter for each of these, so we could bind the installed app name to a "?" in the query. SQL has a limit of (usually) 999 parameters which can be provided to a query, which meant it would fall over if the user had more than 1000 apps installed. This change introduces a new table called "fdroid_installedApps". It is initialized on first run, by iterating over the installed apps as given by the PackageManager. It is subsequenty kept up to date by a set of BroadcastReceivers, which listen for apps being uninstalled/installed/upgraded. It also includes tests to verify that queries of installed apps, when there are more than 1000 apps installed, don't break. Finally, tests are also now able to to insert into providers other than the one under test. This is due to the fact that the providers often join onto tables managed by other providers. --- AndroidManifest.xml | 21 +- src/org/fdroid/fdroid/AppDetails.java | 20 +- src/org/fdroid/fdroid/FDroidApp.java | 39 ++- .../fdroid/fdroid/PackageAddedReceiver.java | 44 ++++ src/org/fdroid/fdroid/PackageReceiver.java | 29 ++- .../fdroid/fdroid/PackageRemovedReceiver.java | 38 +++ .../fdroid/PackageUpgradedReceiver.java | 49 ++++ src/org/fdroid/fdroid/UpdateService.java | 2 +- src/org/fdroid/fdroid/Utils.java | 48 ---- src/org/fdroid/fdroid/data/ApkProvider.java | 1 - src/org/fdroid/fdroid/data/App.java | 48 ++-- src/org/fdroid/fdroid/data/AppProvider.java | 235 ++++++++++++------ src/org/fdroid/fdroid/data/DBHelper.java | 25 +- .../fdroid/fdroid/data/FDroidProvider.java | 8 + .../fdroid/data/InstalledAppCacheUpdater.java | 194 +++++++++++++++ .../fdroid/data/InstalledAppProvider.java | 180 ++++++++++++++ src/org/fdroid/fdroid/data/QueryBuilder.java | 28 ++- .../fdroid/fdroid/views/AppListAdapter.java | 10 +- .../views/fragments/AppListFragment.java | 2 + .../mock/MockInstallablePackageManager.java | 36 ++- .../org/fdroid/fdroid/AppProviderTest.java | 96 +++++-- .../org/fdroid/fdroid/FDroidProviderTest.java | 46 +++- .../fdroid/fdroid/InstalledAppCacheTest.java | 179 +++++++++++++ .../fdroid/InstalledAppProviderTest.java | 168 +++++++++++++ test/src/org/fdroid/fdroid/TestUtils.java | 77 +++++- .../fdroid/mock/MockInstalledApkCache.java | 16 -- 26 files changed, 1369 insertions(+), 270 deletions(-) create mode 100644 src/org/fdroid/fdroid/PackageAddedReceiver.java create mode 100644 src/org/fdroid/fdroid/PackageRemovedReceiver.java create mode 100644 src/org/fdroid/fdroid/PackageUpgradedReceiver.java create mode 100644 src/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java create mode 100644 src/org/fdroid/fdroid/data/InstalledAppProvider.java create mode 100644 test/src/org/fdroid/fdroid/InstalledAppCacheTest.java create mode 100644 test/src/org/fdroid/fdroid/InstalledAppProviderTest.java delete mode 100644 test/src/org/fdroid/fdroid/mock/MockInstalledApkCache.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index cafacbd19..79160e0db 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -61,6 +61,11 @@ android:name="org.fdroid.fdroid.data.ApkProvider" android:exported="false"/> + + - + - + + + + + + + + + + + + + diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 7364e85bf..a49fdf973 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -136,7 +136,7 @@ public class AppDetails extends ListActivity { + " " + apk.version + (apk.vercode == app.suggestedVercode ? " ☆" : "")); - if (apk.vercode == app.getInstalledVerCode(getContext()) + if (apk.vercode == app.installedVersionCode && mInstalledSigID != null && apk.sig != null && apk.sig.equals(mInstalledSigID)) { holder.status.setText(getString(R.string.inst)); @@ -437,7 +437,7 @@ public class AppDetails extends ListActivity { // Get the signature of the installed package... mInstalledSignature = null; mInstalledSigID = null; - if (app.getInstalledVersion(this) != null) { + if (app.isInstalled()) { PackageManager pm = getBaseContext().getPackageManager(); try { PackageInfo pi = pm.getPackageInfo(appid, @@ -624,11 +624,11 @@ public class AppDetails extends ListActivity { adapter.notifyDataSetChanged(); TextView tv = (TextView) findViewById(R.id.status); - if (app.getInstalledVersion(this) == null) + if (!app.isInstalled()) tv.setText(getString(R.string.details_notinstalled)); else tv.setText(getString(R.string.details_installed, - app.getInstalledVersion(this))); + app.installedVersionName)); tv = (TextView) infoView.findViewById(R.id.signature); if (pref_expert && mInstalledSignature != null) { @@ -643,9 +643,9 @@ public class AppDetails extends ListActivity { @Override protected void onListItemClick(ListView l, View v, int position, long id) { final Apk apk = adapter.getItem(position - l.getHeaderViewsCount()); - if (app.getInstalledVerCode(this) == apk.vercode) + if (app.installedVersionCode == apk.vercode) removeApk(app.id); - else if (app.getInstalledVerCode(this) > apk.vercode) { + else if (app.installedVersionCode > apk.vercode) { AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this); ask_alrt.setMessage(getString(R.string.installDowngrade)); ask_alrt.setPositiveButton(getString(R.string.yes), @@ -676,7 +676,7 @@ public class AppDetails extends ListActivity { menu.clear(); if (app == null) return true; - if (app.canAndWantToUpdate(this)) { + if (app.canAndWantToUpdate()) { MenuItemCompat.setShowAsAction(menu.add( Menu.NONE, INSTALL, 0, R.string.menu_upgrade) .setIcon(R.drawable.ic_menu_refresh), @@ -685,14 +685,14 @@ public class AppDetails extends ListActivity { } // Check count > 0 due to incompatible apps resulting in an empty list. - if (app.getInstalledVersion(this) == null && app.suggestedVercode > 0 && + if (!app.isInstalled() && app.suggestedVercode > 0 && adapter.getCount() > 0) { MenuItemCompat.setShowAsAction(menu.add( Menu.NONE, INSTALL, 1, R.string.menu_install) .setIcon(android.R.drawable.ic_menu_add), MenuItemCompat.SHOW_AS_ACTION_ALWAYS | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT); - } else if (app.getInstalledVersion(this) != null) { + } else if (app.isInstalled()) { MenuItemCompat.setShowAsAction(menu.add( Menu.NONE, UNINSTALL, 1, R.string.menu_uninstall) .setIcon(android.R.drawable.ic_menu_delete), @@ -719,7 +719,7 @@ public class AppDetails extends ListActivity { .setCheckable(true) .setChecked(app.ignoreAllUpdates); - if (app.hasUpdates(this)) { + if (app.hasUpdates()) { menu.add(Menu.NONE, IGNORETHIS, 2, R.string.menu_ignore_this) .setIcon(android.R.drawable.ic_menu_close_clear_cancel) .setCheckable(true) diff --git a/src/org/fdroid/fdroid/FDroidApp.java b/src/org/fdroid/fdroid/FDroidApp.java index e0d6f2925..66d54a219 100644 --- a/src/org/fdroid/fdroid/FDroidApp.java +++ b/src/org/fdroid/fdroid/FDroidApp.java @@ -18,42 +18,30 @@ package org.fdroid.fdroid; -import java.io.File; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Semaphore; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - import android.app.Activity; import android.app.Application; -import android.content.Context; import android.content.SharedPreferences; - import android.preference.PreferenceManager; import android.util.Log; - import com.nostra13.universalimageloader.cache.disc.impl.LimitedAgeDiscCache; import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; import com.nostra13.universalimageloader.utils.StorageUtils; - import de.duenndns.ssl.MemorizingTrustManager; - -import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.compat.PRNGFixes; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.InstalledAppCacheUpdater; import org.thoughtcrime.ssl.pinning.PinningTrustManager; import org.thoughtcrime.ssl.pinning.SystemKeyStore; +import javax.net.ssl.*; +import java.io.File; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; + public class FDroidApp extends Application { private static enum Theme { @@ -91,9 +79,12 @@ public class FDroidApp extends Application { //Apply the Google PRNG fixes to properly seed SecureRandom PRNGFixes.apply(); - // Set this up here, and the testing framework will override it when - // it gets fired up. - Utils.setupInstalledApkCache(new Utils.InstalledApkCache()); + // Check that the installed app cache hasn't gotten out of sync somehow. + // e.g. if we crashed/ran out of battery half way through responding + // to a package installed intent. It doesn't really matter where + // we put this in the bootstrap process, because it runs on a different + // thread. In fact, we may as well start early for this reason. + InstalledAppCacheUpdater.updateInBackground(getApplicationContext()); // If the user changes the preference to do with filtering rooted apps, // it is easier to just notify a change in the app provider, diff --git a/src/org/fdroid/fdroid/PackageAddedReceiver.java b/src/org/fdroid/fdroid/PackageAddedReceiver.java new file mode 100644 index 000000000..5d218b00c --- /dev/null +++ b/src/org/fdroid/fdroid/PackageAddedReceiver.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2014 Peter Serwylo, peter@serwylo.com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.fdroid.fdroid; + +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.net.Uri; +import android.util.Log; +import org.fdroid.fdroid.data.InstalledAppProvider; + +public class PackageAddedReceiver extends PackageReceiver { + + @Override + protected void handle(Context context, String appId) { + PackageInfo info = getPackageInfo(context, appId); + + Log.d("FDroid", "Inserting installed app info for '" + appId + "' (v" + info.versionCode + ")"); + + Uri uri = InstalledAppProvider.getContentUri(); + ContentValues values = new ContentValues(3); + values.put(InstalledAppProvider.DataColumns.APP_ID, appId); + values.put(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode); + values.put(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName); + context.getContentResolver().insert(uri, values); + } + +} \ No newline at end of file diff --git a/src/org/fdroid/fdroid/PackageReceiver.java b/src/org/fdroid/fdroid/PackageReceiver.java index 08aebc755..00b85227f 100644 --- a/src/org/fdroid/fdroid/PackageReceiver.java +++ b/src/org/fdroid/fdroid/PackageReceiver.java @@ -1,5 +1,6 @@ /* - * Copyright (C) 2012 Ciaran Gultnieks, ciaran@ciarang.com + * Copyright (C) 2014 Ciaran Gultnieks, ciaran@ciarang.com, + * Peter Serwylo, peter@serwylo.com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -21,17 +22,31 @@ package org.fdroid.fdroid; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageInfo; import android.util.Log; +import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.AppProvider; -public class PackageReceiver extends BroadcastReceiver { +abstract class PackageReceiver extends BroadcastReceiver { + + abstract protected void handle(Context context, String appId); + + protected PackageInfo getPackageInfo(Context context, String appId) { + for( PackageInfo info : context.getPackageManager().getInstalledPackages(0)) { + if (info.packageName.equals(appId)) { + return info; + } + } + return null; + } @Override - public void onReceive(Context ctx, Intent intent) { - String appid = intent.getData().getSchemeSpecificPart(); - Log.d("FDroid", "PackageReceiver received "+appid); - ctx.getContentResolver().notifyChange(AppProvider.getContentUri(appid), null); - Utils.clearInstalledApksCache(); + public void onReceive(Context context, Intent intent) { + Log.d("FDroid", "PackageReceiver received [action = '" + intent.getAction() + "', data = '" + intent.getData() + "']"); + String appId = intent.getData().getSchemeSpecificPart(); + handle(context, appId); + context.getContentResolver().notifyChange(AppProvider.getContentUri(appId), null); + context.getContentResolver().notifyChange(ApkProvider.getAppUri(appId), null); } } diff --git a/src/org/fdroid/fdroid/PackageRemovedReceiver.java b/src/org/fdroid/fdroid/PackageRemovedReceiver.java new file mode 100644 index 000000000..163a8278b --- /dev/null +++ b/src/org/fdroid/fdroid/PackageRemovedReceiver.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2014 Peter Serwylo, peter@serwylo.com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.fdroid.fdroid; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.InstalledAppProvider; + +public class PackageRemovedReceiver extends PackageReceiver { + + @Override + protected void handle(Context context, String appId) { + + Log.d("FDroid", "Removing installed app info for '" + appId + "'"); + + Uri uri = InstalledAppProvider.getAppUri(appId); + context.getContentResolver().delete(uri, null, null); + } + +} \ No newline at end of file diff --git a/src/org/fdroid/fdroid/PackageUpgradedReceiver.java b/src/org/fdroid/fdroid/PackageUpgradedReceiver.java new file mode 100644 index 000000000..516a9660d --- /dev/null +++ b/src/org/fdroid/fdroid/PackageUpgradedReceiver.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 Peter Serwylo, peter@serwylo.com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.fdroid.fdroid; + +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.net.Uri; +import android.util.Log; +import org.fdroid.fdroid.data.InstalledAppProvider; + +/** + * For some reason, devices seem to be keen on sending a REMOVED and then an INSTALLED + * intent, rather than an CHANGED intent. Therefore, this is probably not used on many + * devices. Regardless, it is tested in the unit tests and should work on devices that + * opt instead to send the PACKAGE_CHANGED intent. + */ +public class PackageUpgradedReceiver extends PackageReceiver { + + @Override + protected void handle(Context context, String appId) { + PackageInfo info = getPackageInfo(context, appId); + + Log.d("FDroid", "Updating installed app info for '" + appId + "' to v" + info.versionCode + " (" + info.versionName + ")"); + + Uri uri = InstalledAppProvider.getAppUri(appId); + ContentValues values = new ContentValues(1); + values.put(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode); + values.put(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName); + context.getContentResolver().update(uri, values, null, null); + } + +} \ No newline at end of file diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index 35b603aad..d0c74735f 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -405,7 +405,7 @@ public class UpdateService extends IntentService implements ProgressListener { break; } } - if (!ignored && app.hasUpdates(this)) { + if (!ignored && app.hasUpdates()) { updateCount++; } } diff --git a/src/org/fdroid/fdroid/Utils.java b/src/org/fdroid/fdroid/Utils.java index b5fa3a37b..62409bd15 100644 --- a/src/org/fdroid/fdroid/Utils.java +++ b/src/org/fdroid/fdroid/Utils.java @@ -195,14 +195,6 @@ public final class Utils { return apkCacheDir; } - public static Map getInstalledApps(Context context) { - return installedApkCache.getApks(context); - } - - public static void clearInstalledApksCache() { - installedApkCache.emptyCache(); - } - public static String calcFingerprint(String keyHexString) { if (TextUtils.isEmpty(keyHexString)) return null; @@ -296,44 +288,4 @@ public final class Utils { } } - private static InstalledApkCache installedApkCache = null; - - /** - * We do a lot of querying of the installed app's. As a result, we like - * to cache this information quite heavily (and flush the cache when new - * apps are installed). The caching implementation needs to be setup like - * this so that it is possible to mock for testing purposes. - */ - public static void setupInstalledApkCache(InstalledApkCache cache) { - installedApkCache = cache; - } - - public static class InstalledApkCache { - - protected Map installedApks = null; - - protected Map buildAppList(Context context) { - Map info = new HashMap(); - Log.d("FDroid", "Reading installed packages"); - List installedPackages = context.getPackageManager().getInstalledPackages(0); - for (PackageInfo appInfo : installedPackages) { - info.put(appInfo.packageName, appInfo); - } - return info; - } - - public Map getApks(Context context) { - if (installedApks == null) { - installedApks = buildAppList(context); - } - return installedApks; - } - - public void emptyCache() { - installedApks = null; - } - - } - - } diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java index 9f4f7630b..80b88ef87 100644 --- a/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -8,7 +8,6 @@ import android.database.Cursor; import android.net.Uri; import android.provider.BaseColumns; import android.util.Log; -import org.fdroid.fdroid.UpdateService; import java.util.*; diff --git a/src/org/fdroid/fdroid/data/App.java b/src/org/fdroid/fdroid/data/App.java index 30688b21a..b5ffb8292 100644 --- a/src/org/fdroid/fdroid/data/App.java +++ b/src/org/fdroid/fdroid/data/App.java @@ -79,6 +79,10 @@ public class App extends ValueObject implements Comparable { public String iconUrl; + public String installedVersionName; + + public int installedVersionCode; + @Override public int compareTo(App app) { return name.compareToIgnoreCase(app.name); @@ -148,6 +152,10 @@ public class App extends ValueObject implements Comparable { ignoreThisUpdate = cursor.getInt(i); } else if (column.equals(AppProvider.DataColumns.ICON_URL)) { iconUrl = cursor.getString(i); + } else if (column.equals(AppProvider.DataColumns.InstalledApp.VERSION_CODE)) { + installedVersionCode = cursor.getInt(i); + } else if (column.equals(AppProvider.DataColumns.InstalledApp.VERSION_NAME)) { + installedVersionName = cursor.getString(i); } } } @@ -186,53 +194,25 @@ public class App extends ValueObject implements Comparable { return values; } - /** - * Version string for the currently installed version of this apk. - * If not installed, returns null. - */ - public String getInstalledVersion(Context context) { - PackageInfo info = getInstalledInfo(context); - return info == null ? null : info.versionName; - } - - /** - * Version code for the currently installed version of this apk. - * If not installed, it returns -1. - */ - public int getInstalledVerCode(Context context) { - PackageInfo info = getInstalledInfo(context); - return info == null ? -1 : info.versionCode; - } - - /** - * True if installed by the user, false if a system apk or not installed. - */ - public boolean getUserInstalled(Context context) { - PackageInfo info = getInstalledInfo(context); - return info != null && ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 1); - } - - public PackageInfo getInstalledInfo(Context context) { - Map installed = Utils.getInstalledApps(context); - return installed.containsKey(id) ? installed.get(id) : null; + public boolean isInstalled() { + return installedVersionCode > 0; } /** * True if there are new versions (apks) available */ - public boolean hasUpdates(Context context) { + public boolean hasUpdates() { boolean updates = false; if (suggestedVercode > 0) { - int installedVerCode = getInstalledVerCode(context); - updates = (installedVerCode > 0 && installedVerCode < suggestedVercode); + updates = (installedVersionCode > 0 && installedVersionCode < suggestedVercode); } return updates; } // True if there are new versions (apks) available and the user wants // to be notified about them - public boolean canAndWantToUpdate(Context context) { - boolean canUpdate = hasUpdates(context); + public boolean canAndWantToUpdate() { + boolean canUpdate = hasUpdates(); boolean wantsUpdate = !ignoreAllUpdates && ignoreThisUpdate < suggestedVercode; return canUpdate && wantsUpdate && !isFiltered(); } diff --git a/src/org/fdroid/fdroid/data/AppProvider.java b/src/org/fdroid/fdroid/data/AppProvider.java index 9bd19d476..5298f3efa 100644 --- a/src/org/fdroid/fdroid/data/AppProvider.java +++ b/src/org/fdroid/fdroid/data/AppProvider.java @@ -1,7 +1,6 @@ package org.fdroid.fdroid.data; import android.content.*; -import android.content.pm.PackageInfo; import android.database.Cursor; import android.net.Uri; import android.util.Log; @@ -13,9 +12,6 @@ import java.util.*; public class AppProvider extends FDroidProvider { - /** - * @see org.fdroid.fdroid.data.ApkProvider.MAX_APKS_TO_QUERY - */ public static final int MAX_APPS_TO_QUERY = 900; public static final class Helper { @@ -166,6 +162,11 @@ public class AppProvider extends FDroidProvider { public static final String VERSION = "suggestedApkVersion"; } + public interface InstalledApp { + public static final String VERSION_CODE = "installedVersionCode"; + public static final String VERSION_NAME = "installedVersionName"; + } + public static String[] ALL = { IS_COMPATIBLE, APP_ID, NAME, SUMMARY, ICON, DESCRIPTION, LICENSE, WEB_URL, TRACKER_URL, SOURCE_URL, DONATE_URL, @@ -173,14 +174,76 @@ public class AppProvider extends FDroidProvider { UPSTREAM_VERSION, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED, CATEGORIES, ANTI_FEATURES, REQUIREMENTS, IGNORE_ALLUPDATES, IGNORE_THISUPDATE, ICON_URL, SUGGESTED_VERSION_CODE, - SuggestedApk.VERSION + SuggestedApk.VERSION, InstalledApp.VERSION_CODE, + InstalledApp.VERSION_NAME }; } + /** + * A QuerySelection which is aware of the option/need to join onto the + * installed apps table. Not that the base classes + * {@link org.fdroid.fdroid.data.QuerySelection#add(QuerySelection)} and + * {@link org.fdroid.fdroid.data.QuerySelection#add(String, String[])} methods + * will only return the base class {@link org.fdroid.fdroid.data.QuerySelection} + * which is not aware of the installed app table. + * However, the + * {@link org.fdroid.fdroid.data.AppProvider.AppQuerySelection#add(org.fdroid.fdroid.data.AppProvider.AppQuerySelection)} + * method from this class will return an instance of this class, that is aware of + * the install apps table. + */ + private static class AppQuerySelection extends QuerySelection { + + private boolean naturalJoinToInstalled = false; + + public AppQuerySelection() { + // The same as no selection, because "1" will always resolve to true when executing the SQL query. + // e.g. "WHERE 1 AND ..." is the same as "WHERE ..." + super("1"); + } + + public AppQuerySelection(String selection) { + super(selection); + } + + public AppQuerySelection(String selection, String[] args) { + super(selection, args); + } + + public AppQuerySelection(String selection, List args) { + super(selection, args); + } + + public boolean naturalJoinToInstalled() { + return naturalJoinToInstalled; + } + + /** + * Tells the query selection that it will need to join onto the installed apps table + * when used. This should be called when your query makes use of fields from that table + * (for example, list all installed, or list those which can be updated). + * @return A reference to this object, to allow method chaining, for example + * return new AppQuerySelection(selection).requiresInstalledTable()) + */ + public AppQuerySelection requireNaturalInstalledTable() { + naturalJoinToInstalled = true; + return this; + } + + public AppQuerySelection add(AppQuerySelection query) { + QuerySelection both = super.add(query); + AppQuerySelection bothWithJoin = new AppQuerySelection(both.getSelection(), both.getArgs()); + if (this.naturalJoinToInstalled() || query.naturalJoinToInstalled()) { + bothWithJoin.requireNaturalInstalledTable(); + } + return bothWithJoin; + } + + } + private static class Query extends QueryBuilder { private boolean isSuggestedApkTableAdded = false; - + private boolean requiresInstalledTable = false; private boolean categoryFieldAdded = false; @Override @@ -193,10 +256,43 @@ public class AppProvider extends FDroidProvider { return fieldCount() == 1 && categoryFieldAdded; } + public void addSelection(AppQuerySelection selection) { + addSelection(selection.getSelection()); + if (selection.naturalJoinToInstalled()) { + naturalJoinToInstalledTable(); + } + } + + // TODO: What if the selection requires a natural join, but we first get a left join + // because something causes leftJoin to be caused first? Maybe throw an exception? + public void naturalJoinToInstalledTable() { + if (!requiresInstalledTable) { + join( + DBHelper.TABLE_INSTALLED_APP, + "installed", + "installed." + InstalledAppProvider.DataColumns.APP_ID + " = " + DBHelper.TABLE_APP + ".id"); + requiresInstalledTable = true; + } + } + + public void leftJoinToInstalledTable() { + if (!requiresInstalledTable) { + leftJoin( + DBHelper.TABLE_INSTALLED_APP, + "installed", + "installed." + InstalledAppProvider.DataColumns.APP_ID + " = " + DBHelper.TABLE_APP + ".id"); + requiresInstalledTable = true; + } + } + @Override public void addField(String field) { if (field.equals(DataColumns.SuggestedApk.VERSION)) { addSuggestedApkVersionField(); + } else if (field.equals(DataColumns.InstalledApp.VERSION_NAME)) { + addInstalledAppVersionName(); + } else if (field.equals(DataColumns.InstalledApp.VERSION_CODE)) { + addInstalledAppVersionCode(); } else if (field.equals(DataColumns._COUNT)) { appendCountField(); } else { @@ -227,6 +323,25 @@ public class AppProvider extends FDroidProvider { } appendField(fieldName, "suggestedApk", alias); } + + private void addInstalledAppVersionName() { + addInstalledAppField( + InstalledAppProvider.DataColumns.VERSION_NAME, + DataColumns.InstalledApp.VERSION_NAME + ); + } + + private void addInstalledAppVersionCode() { + addInstalledAppField( + InstalledAppProvider.DataColumns.VERSION_CODE, + DataColumns.InstalledApp.VERSION_CODE + ); + } + + private void addInstalledAppField(String fieldName, String alias) { + leftJoinToInstalledTable(); + appendField(fieldName, "installed", alias); + } } private static final String PROVIDER_NAME = "AppProvider"; @@ -242,7 +357,6 @@ public class AppProvider extends FDroidProvider { private static final String PATH_NEWLY_ADDED = "newlyAdded"; private static final String PATH_CATEGORY = "category"; private static final String PATH_IGNORED = "ignored"; - private static final String PATH_CALC_APP_DETAILS_FROM_INDEX = "calcDetailsFromIndex"; private static final int CAN_UPDATE = CODE_SINGLE + 1; @@ -254,7 +368,6 @@ public class AppProvider extends FDroidProvider { private static final int NEWLY_ADDED = RECENTLY_UPDATED + 1; private static final int CATEGORY = NEWLY_ADDED + 1; private static final int IGNORED = CATEGORY + 1; - private static final int CALC_APP_DETAILS_FROM_INDEX = IGNORED + 1; static { @@ -359,49 +472,19 @@ public class AppProvider extends FDroidProvider { return matcher; } - private QuerySelection queryCanUpdate() { - Map installedApps = Utils.getInstalledApps(getContext()); - + private AppQuerySelection queryCanUpdate() { String ignoreCurrent = " fdroid_app.ignoreThisUpdate != fdroid_app.suggestedVercode "; String ignoreAll = " fdroid_app.ignoreAllUpdates != 1 "; String ignore = " ( " + ignoreCurrent + " AND " + ignoreAll + " ) "; - - StringBuilder where = new StringBuilder( ignore + " AND ( 0 "); - String[] selectionArgs = new String[installedApps.size() * 2]; - int i = 0; - for (PackageInfo info : installedApps.values() ) { - where.append(" OR ( fdroid_app.") - .append(DataColumns.APP_ID) - .append(" = ? AND fdroid_app.") - .append(DataColumns.SUGGESTED_VERSION_CODE) - .append(" > ?) "); - selectionArgs[ i * 2 ] = info.packageName; - selectionArgs[ i * 2 + 1 ] = Integer.toString(info.versionCode); - i ++; - } - where.append(") "); - - return new QuerySelection(where.toString(), selectionArgs); + String where = ignore + " AND fdroid_app." + DataColumns.SUGGESTED_VERSION_CODE + " > installed.versionCode"; + return new AppQuerySelection(where).requireNaturalInstalledTable(); } - private QuerySelection queryInstalled() { - Map installedApps = Utils.getInstalledApps(getContext()); - StringBuilder where = new StringBuilder( " ( 0 "); - String[] selectionArgs = new String[installedApps.size()]; - int i = 0; - for (Map.Entry entry : installedApps.entrySet() ) { - where.append(" OR fdroid_app.") - .append(AppProvider.DataColumns.APP_ID) - .append(" = ? "); - selectionArgs[i] = entry.getKey(); - i ++; - } - where.append(" ) "); - - return new QuerySelection(where.toString(), selectionArgs); + private AppQuerySelection queryInstalled() { + return new AppQuerySelection().requireNaturalInstalledTable(); } - private QuerySelection querySearch(String keywords) { + private AppQuerySelection querySearch(String keywords) { keywords = "%" + keywords + "%"; String selection = "fdroid_app.id like ? OR " + @@ -409,34 +492,34 @@ public class AppProvider extends FDroidProvider { "fdroid_app.summary like ? OR " + "fdroid_app.description like ? "; String[] args = new String[] { keywords, keywords, keywords, keywords}; - return new QuerySelection(selection, args); + return new AppQuerySelection(selection, args); } - private QuerySelection querySingle(String id) { + private AppQuerySelection querySingle(String id) { String selection = "fdroid_app.id = ?"; String[] args = { id }; - return new QuerySelection(selection, args); + return new AppQuerySelection(selection, args); } - private QuerySelection queryIgnored() { + private AppQuerySelection queryIgnored() { String selection = "fdroid_app.ignoreAllUpdates = 1 OR " + "fdroid_app.ignoreThisUpdate >= fdroid_app.suggestedVercode"; - return new QuerySelection(selection); + return new AppQuerySelection(selection); } - private QuerySelection queryNewlyAdded() { + private AppQuerySelection queryNewlyAdded() { String selection = "fdroid_app.added > ?"; String[] args = { Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory()) }; - return new QuerySelection(selection, args); + return new AppQuerySelection(selection, args); } - private QuerySelection queryRecentlyUpdated() { + private AppQuerySelection queryRecentlyUpdated() { String selection = "fdroid_app.added != fdroid_app.lastUpdated AND fdroid_app.lastUpdated > ?"; String[] args = { Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory()) }; - return new QuerySelection(selection, args); + return new AppQuerySelection(selection, args); } - private QuerySelection queryCategory(String category) { + private AppQuerySelection queryCategory(String category) { // TODO: In the future, add a new table for categories, // so we can join onto it. String selection = @@ -450,67 +533,68 @@ public class AppProvider extends FDroidProvider { "%," + category, "%," + category + ",%", }; - return new QuerySelection(selection, args); + return new AppQuerySelection(selection, args); } - private QuerySelection queryNoApks() { + private AppQuerySelection queryNoApks() { String selection = "(SELECT COUNT(*) FROM fdroid_apk WHERE fdroid_apk.id = fdroid_app.id) = 0"; - return new QuerySelection(selection); + return new AppQuerySelection(selection); } - private QuerySelection queryApps(String appIds) { + private AppQuerySelection queryApps(String appIds) { String[] args = appIds.split(","); String selection = "fdroid_app.id IN (" + generateQuestionMarksForInClause(args.length) + ")"; - return new QuerySelection(selection, args); + return new AppQuerySelection(selection, args); } @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - QuerySelection query = new QuerySelection(selection, selectionArgs); + public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) { + Query query = new Query(); + AppQuerySelection selection = new AppQuerySelection(customSelection, selectionArgs); switch (matcher.match(uri)) { case CODE_LIST: break; case CODE_SINGLE: - query = query.add(querySingle(uri.getLastPathSegment())); + selection = selection.add(querySingle(uri.getLastPathSegment())); break; case CAN_UPDATE: - query = query.add(queryCanUpdate()); + selection = selection.add(queryCanUpdate()); break; case INSTALLED: - query = query.add(queryInstalled()); + selection = selection.add(queryInstalled()); break; case SEARCH: - query = query.add(querySearch(uri.getLastPathSegment())); + selection = selection.add(querySearch(uri.getLastPathSegment())); break; case NO_APKS: - query = query.add(queryNoApks()); + selection = selection.add(queryNoApks()); break; case APPS: - query = query.add(queryApps(uri.getLastPathSegment())); + selection = selection.add(queryApps(uri.getLastPathSegment())); break; case IGNORED: - query = query.add(queryIgnored()); + selection = selection.add(queryIgnored()); break; case CATEGORY: - query = query.add(queryCategory(uri.getLastPathSegment())); + selection = selection.add(queryCategory(uri.getLastPathSegment())); break; case RECENTLY_UPDATED: sortOrder = " fdroid_app.lastUpdated DESC"; - query = query.add(queryRecentlyUpdated()); + selection = selection.add(queryRecentlyUpdated()); break; case NEWLY_ADDED: sortOrder = " fdroid_app.added DESC"; - query = query.add(queryNewlyAdded()); + selection = selection.add(queryNewlyAdded()); break; default: @@ -522,12 +606,11 @@ public class AppProvider extends FDroidProvider { sortOrder = " lower( fdroid_app." + sortOrder + " ) "; } - Query q = new Query(); - q.addFields(projection); - q.addSelection(query.getSelection()); - q.addOrderBy(sortOrder); + query.addSelection(selection); + query.addFields(projection); // TODO: Make the order of addFields/addSelection not dependent on each other... + query.addOrderBy(sortOrder); - Cursor cursor = read().rawQuery(q.toString(), query.getArgs()); + Cursor cursor = read().rawQuery(query.toString(), selection.getArgs()); cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; } diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index c75fe67e9..49cfda93a 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -2,18 +2,20 @@ package org.fdroid.fdroid.data; import android.content.ContentValues; import android.content.Context; -import android.content.res.Resources; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; -import org.fdroid.fdroid.*; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.Utils; import java.util.ArrayList; import java.util.List; public class DBHelper extends SQLiteOpenHelper { + private static final String TAG = "org.fdroid.fdroid.data.DBHelper"; + public static final String DATABASE_NAME = "fdroid"; public static final String TABLE_REPO = "fdroid_repo"; @@ -23,7 +25,6 @@ public class DBHelper extends SQLiteOpenHelper { // This information is retrieved from the repositories. public static final String TABLE_APK = "fdroid_apk"; - private static final String CREATE_TABLE_REPO = "create table " + TABLE_REPO + " (_id integer primary key, " + "address text not null, " @@ -87,7 +88,15 @@ public class DBHelper extends SQLiteOpenHelper { + "iconUrl text, " + "primary key(id));"; - private static final int DB_VERSION = 42; + public static final String TABLE_INSTALLED_APP = "fdroid_installedApp"; + private static final String CREATE_TABLE_INSTALLED_APP = "CREATE TABLE " + TABLE_INSTALLED_APP + + " ( " + + "appId TEXT NOT NULL PRIMARY KEY, " + + "versionCode INT NOT NULL, " + + "versionName TEXT NOT NULL " + + " );"; + + private static final int DB_VERSION = 43; private Context context; @@ -177,6 +186,7 @@ public class DBHelper extends SQLiteOpenHelper { public void onCreate(SQLiteDatabase db) { createAppApk(db); + createInstalledApp(db); db.execSQL(CREATE_TABLE_REPO); insertRepo( @@ -239,6 +249,8 @@ public class DBHelper extends SQLiteOpenHelper { addLastUpdatedToRepo(db, oldVersion); renameRepoId(db, oldVersion); populateRepoNames(db, oldVersion); + + if (oldVersion < 43) createInstalledApp(db); } /** @@ -381,6 +393,11 @@ public class DBHelper extends SQLiteOpenHelper { db.execSQL("create index apk_id on " + TABLE_APK + " (id);"); } + private void createInstalledApp(SQLiteDatabase db) { + Log.d(TAG, "Creating 'installed app' database table."); + db.execSQL(CREATE_TABLE_INSTALLED_APP); + } + private static boolean columnExists(SQLiteDatabase db, String table, String column) { return (db.rawQuery( "select * from " + table + " limit 0,1", null ) diff --git a/src/org/fdroid/fdroid/data/FDroidProvider.java b/src/org/fdroid/fdroid/data/FDroidProvider.java index 7ae36ff51..aad493d4a 100644 --- a/src/org/fdroid/fdroid/data/FDroidProvider.java +++ b/src/org/fdroid/fdroid/data/FDroidProvider.java @@ -26,6 +26,14 @@ public abstract class FDroidProvider extends ContentProvider { abstract protected String getProviderName(); + /** + * Should always be the same as the provider:name in the AndroidManifest + * @return + */ + public final String getName() { + return AUTHORITY + "." + getProviderName(); + } + /** * Tells us if we are in the middle of a batch of operations. Allows us to * decide not to notify the content resolver of changes, diff --git a/src/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java b/src/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java new file mode 100644 index 000000000..7d7b65d22 --- /dev/null +++ b/src/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java @@ -0,0 +1,194 @@ +package org.fdroid.fdroid.data; + +import android.content.ContentProviderOperation; +import android.content.Context; +import android.content.OperationApplicationException; +import android.content.pm.PackageInfo; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.RemoteException; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Compares what is in the fdroid_installedApp SQLite database table with the package + * info that we can gleam from the {@link android.content.pm.PackageManager}. If there + * is any updates/removals/insertions which need to take place, we will perform them. + * TODO: The content providers are not thread safe, so it is possible we will be writing + * to the database at the same time we respond to a broadcasted intent. + */ +public class InstalledAppCacheUpdater { + + private static final String TAG = "org.fdroid.fdroid.data.InstalledAppCacheUpdater"; + + private Context context; + + private List toInsert = new ArrayList(); + private List toUpdate = new ArrayList(); + private List toDelete = new ArrayList(); + + protected InstalledAppCacheUpdater(Context context) { + this.context = context; + } + + /** + * Ensure our database of installed apps is in sync with what the PackageManager tells us is installed. + * Once completed, the relevant ContentProviders will be notified of any changes to installed statuses. + * This method will block until completed, which could be in the order of a few seconds (depending on + * how many apps are installed). + */ + public static void updateInForeground(Context context) { + InstalledAppCacheUpdater updater = new InstalledAppCacheUpdater(context); + if (updater.update()) { + updater.notifyProviders(); + } + } + + /** + * Ensure our database of installed apps is in sync with what the PackageManager tells us is installed. + * Once completed, the relevant ContentProviders will be notified of any changes to installed statuses. + * This method returns immediately, and will continue to work in an AsyncTask. + */ + public static void updateInBackground(Context context) { + InstalledAppCacheUpdater updater = new InstalledAppCacheUpdater(context); + updater.startBackgroundWorker(); + } + + protected boolean update() { + + long startTime = System.currentTimeMillis(); + + compareCacheToPackageManager(); + updateCache(); + + long duration = System.currentTimeMillis() - startTime; + Log.d(TAG, "Took " + duration + "ms to compare the installed app cache with PackageManager."); + + return hasChanged(); + } + + protected void notifyProviders() { + Log.i(TAG, "Installed app cache has changed, notifying content providers (so they can update the relevant views)."); + context.getContentResolver().notifyChange(AppProvider.getContentUri(), null); + context.getContentResolver().notifyChange(ApkProvider.getContentUri(), null); + } + + protected void startBackgroundWorker() { + new Worker().execute(); + } + + /** + * If any of the cached app details have been removed, updated or inserted, + * then the cache has changed. + */ + private boolean hasChanged() { + return toInsert.size() > 0 || toUpdate.size() > 0 || toDelete.size() > 0; + } + + private void updateCache() { + + ArrayList ops = new ArrayList(); + ops.addAll(deleteFromCache(toDelete)); + ops.addAll(updateCachedValues(toUpdate)); + ops.addAll(insertIntoCache(toInsert)); + + if (ops.size() > 0) { + try { + context.getContentResolver().applyBatch(InstalledAppProvider.getAuthority(), ops); + Log.d(TAG, "Finished executing " + ops.size() + " CRUD operations on installed app cache."); + } catch (RemoteException e) { + Log.e(TAG, "Error updating installed app cache: " + e); + } catch (OperationApplicationException e) { + Log.e(TAG, "Error updating installed app cache: " + e); + } + } + + } + + private void compareCacheToPackageManager() { + + Map cachedInfo = InstalledAppProvider.Helper.all(context); + + List installedPackages = context.getPackageManager().getInstalledPackages(0); + for (PackageInfo appInfo : installedPackages) { + if (!cachedInfo.containsKey(appInfo.packageName)) { + toInsert.add(appInfo); + } else { + if (cachedInfo.get(appInfo.packageName) < appInfo.versionCode) { + toUpdate.add(appInfo); + } + cachedInfo.remove(appInfo.packageName); + } + } + + if (cachedInfo.size() > 0) { + for (Map.Entry entry : cachedInfo.entrySet() ) { + toDelete.add(entry.getKey()); + } + } + } + + private List insertIntoCache(List appsToInsert) { + List ops = new ArrayList(appsToInsert.size()); + if (appsToInsert.size() > 0) { + Log.d(TAG, "Preparing to cache installed info for " + appsToInsert.size() + " new apps."); + Uri uri = InstalledAppProvider.getContentUri(); + for (PackageInfo info : appsToInsert) { + ContentProviderOperation op = ContentProviderOperation.newInsert(uri) + .withValue(InstalledAppProvider.DataColumns.APP_ID, info.packageName) + .withValue(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode) + .withValue(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName) + .build(); + ops.add(op); + } + } + return ops; + } + + private List updateCachedValues(List appsToUpdate) { + List ops = new ArrayList(appsToUpdate.size()); + if (appsToUpdate.size() > 0) { + Log.d(TAG, "Preparing to update installed app cache for " + appsToUpdate.size() + " apps."); + for (PackageInfo info : appsToUpdate) { + Uri uri = InstalledAppProvider.getAppUri(info.packageName); + ContentProviderOperation op = ContentProviderOperation.newUpdate(uri) + .withValue(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode) + .withValue(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName) + .build(); + ops.add(op); + } + } + return ops; + } + + private List deleteFromCache(List appIds) { + List ops = new ArrayList(appIds.size()); + if (appIds.size() > 0) { + Log.d(TAG, "Preparing to remove " + appIds.size() + " apps from the installed app cache."); + for (String appId : appIds) { + Uri uri = InstalledAppProvider.getAppUri(appId); + ops.add(ContentProviderOperation.newDelete(uri).build()); + } + } + return ops; + } + + private class Worker extends AsyncTask { + + @Override + protected Boolean doInBackground(Void... params) { + return update(); + } + + @Override + protected void onPostExecute(Boolean changed) { + if (changed) { + notifyProviders(); + } + } + } + +} diff --git a/src/org/fdroid/fdroid/data/InstalledAppProvider.java b/src/org/fdroid/fdroid/data/InstalledAppProvider.java new file mode 100644 index 000000000..20cacc656 --- /dev/null +++ b/src/org/fdroid/fdroid/data/InstalledAppProvider.java @@ -0,0 +1,180 @@ +package org.fdroid.fdroid.data; + +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; +import org.fdroid.fdroid.R; + +import java.util.HashMap; +import java.util.Map; + +public class InstalledAppProvider extends FDroidProvider { + + public static class Helper { + + /** + * @return The keys are the app ids (package names), and their corresponding values are + * the version code which is installed. + */ + public static Map all(Context context) { + + Map cachedInfo = new HashMap(); + + Uri uri = InstalledAppProvider.getContentUri(); + String[] projection = InstalledAppProvider.DataColumns.ALL; + Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null); + if (cursor != null) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + cachedInfo.put( + cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)), + cursor.getInt(cursor.getColumnIndex(InstalledAppProvider.DataColumns.VERSION_CODE)) + ); + cursor.moveToNext(); + } + cursor.close(); + } + + return cachedInfo; + } + + } + + public interface DataColumns { + + public static final String APP_ID = "appId"; + public static final String VERSION_CODE = "versionCode"; + public static final String VERSION_NAME = "versionName"; + + public static String[] ALL = { APP_ID, VERSION_CODE, VERSION_NAME }; + + } + + private static final String PROVIDER_NAME = "InstalledAppProvider"; + + private static final UriMatcher matcher = new UriMatcher(-1); + + static { + matcher.addURI(getAuthority(), null, CODE_LIST); + matcher.addURI(getAuthority(), "*", CODE_SINGLE); + } + + public static Uri getContentUri() { + return Uri.parse("content://" + getAuthority()); + } + + public static Uri getAppUri(String appId) { + return Uri.withAppendedPath(getContentUri(), appId); + } + + @Override + protected String getTableName() { + return DBHelper.TABLE_INSTALLED_APP; + } + + @Override + protected String getProviderName() { + return "InstalledAppProvider"; + } + + public static String getAuthority() { + return AUTHORITY + "." + PROVIDER_NAME; + } + + @Override + protected UriMatcher getMatcher() { + return matcher; + } + + private QuerySelection queryApp(String appId) { + return new QuerySelection("appId = ?", new String[] { appId } ); + } + + @Override + public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) { + QuerySelection selection = new QuerySelection(customSelection, selectionArgs); + switch (matcher.match(uri)) { + case CODE_LIST: + break; + + case CODE_SINGLE: + selection = selection.add(queryApp(uri.getLastPathSegment())); + break; + + default: + String message = "Invalid URI for installed app content provider: " + uri; + Log.e("FDroid", message); + throw new UnsupportedOperationException(message); + } + + Cursor cursor = read().query(getTableName(), projection, selection.getSelection(), selection.getArgs(), null, null, null); + cursor.setNotificationUri(getContext().getContentResolver(), uri); + return cursor; + } + + @Override + public int delete(Uri uri, String where, String[] whereArgs) { + + if (matcher.match(uri) != CODE_SINGLE) { + throw new UnsupportedOperationException("Delete not supported for " + uri + "."); + } + + QuerySelection query = new QuerySelection(where, whereArgs); + query = query.add(queryApp(uri.getLastPathSegment())); + + int count = write().delete(getTableName(), query.getSelection(), query.getArgs()); + if (!isApplyingBatch()) { + getContext().getContentResolver().notifyChange(uri, null); + } + return count; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + + if (matcher.match(uri) != CODE_LIST) { + throw new UnsupportedOperationException("Insert not supported for " + uri + "."); + } + + verifyVersionNameNotNull(values); + write().insertOrThrow(getTableName(), null, values); + if (!isApplyingBatch()) { + getContext().getContentResolver().notifyChange(uri, null); + } + return getAppUri(values.getAsString(DataColumns.APP_ID)); + } + + @Override + public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { + + if (matcher.match(uri) != CODE_SINGLE) { + throw new UnsupportedOperationException("Update not supported for " + uri + "."); + } + + QuerySelection query = new QuerySelection(where, whereArgs); + query = query.add(queryApp(uri.getLastPathSegment())); + + verifyVersionNameNotNull(values); + int count = write().update(getTableName(), values, query.getSelection(), query.getArgs()); + if (!isApplyingBatch()) { + getContext().getContentResolver().notifyChange(uri, null); + } + return count; + } + + /** + * During development, I stumbled across one (out of over 300) installed apps which had a versionName + * of null. As such, I figured we may as well store it as "Unknown". The alternative is to allow the + * column to accept NULL values in the database, and then deal with the potential of a null everywhere + * "versionName" is used. + */ + private void verifyVersionNameNotNull(ContentValues values) { + if (values.containsKey(DataColumns.VERSION_NAME) && values.getAsString(DataColumns.VERSION_NAME) == null) { + values.put(DataColumns.VERSION_NAME, getContext().getString(R.string.unknown)); + } + } + +} diff --git a/src/org/fdroid/fdroid/data/QueryBuilder.java b/src/org/fdroid/fdroid/data/QueryBuilder.java index 6c988efc0..63cb4dd37 100644 --- a/src/org/fdroid/fdroid/data/QueryBuilder.java +++ b/src/org/fdroid/fdroid/data/QueryBuilder.java @@ -62,17 +62,27 @@ abstract class QueryBuilder { this.orderBy = orderBy; } - protected final void leftJoin(String table, String alias, - String condition) { - tables.append(" LEFT JOIN "); - tables.append(table); + protected final void leftJoin(String table, String alias, String condition) { + joinWithType("LEFT", table, alias, condition); + } + + protected final void join(String table, String alias, String condition) { + joinWithType("", table, alias, condition); + } + + private void joinWithType(String type, String table, String alias, String condition) { + tables.append(' ') + .append(type) + .append(" JOIN ") + .append(table); + if (alias != null) { - tables.append(" AS "); - tables.append(alias); + tables.append(" AS ").append(alias); } - tables.append(" ON ("); - tables.append(condition); - tables.append(")"); + + tables.append(" ON (") + .append(condition) + .append(')'); } private String fieldsSql() { diff --git a/src/org/fdroid/fdroid/views/AppListAdapter.java b/src/org/fdroid/fdroid/views/AppListAdapter.java index d7c179c49..8396d5a57 100644 --- a/src/org/fdroid/fdroid/views/AppListAdapter.java +++ b/src/org/fdroid/fdroid/views/AppListAdapter.java @@ -126,16 +126,14 @@ abstract public class AppListAdapter extends CursorAdapter { return null; } - PackageInfo installedInfo = app.getInstalledInfo(mContext); - - if (installedInfo == null) { + if (!app.isInstalled()) { return app.getSuggestedVersion(); } - String installedVersionString = installedInfo.versionName; - int installedVersionCode = installedInfo.versionCode; + String installedVersionString = app.installedVersionName; + int installedVersionCode = app.installedVersionCode; - if (app.canAndWantToUpdate(mContext) && showStatusUpdate()) { + if (app.canAndWantToUpdate() && showStatusUpdate()) { return installedVersionString + " → " + app.getSuggestedVersion(); } diff --git a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java index f2cfc9f93..d2fd1d9e9 100644 --- a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java @@ -35,6 +35,8 @@ abstract public class AppListFragment extends ListFragment implements AppProvider.DataColumns.LICENSE, AppProvider.DataColumns.ICON, AppProvider.DataColumns.ICON_URL, + AppProvider.DataColumns.InstalledApp.VERSION_CODE, + AppProvider.DataColumns.InstalledApp.VERSION_NAME, AppProvider.DataColumns.SuggestedApk.VERSION, AppProvider.DataColumns.SUGGESTED_VERSION_CODE, AppProvider.DataColumns.IGNORE_ALLUPDATES, diff --git a/test/src/mock/MockInstallablePackageManager.java b/test/src/mock/MockInstallablePackageManager.java index bd47d86f4..f291c1517 100644 --- a/test/src/mock/MockInstallablePackageManager.java +++ b/test/src/mock/MockInstallablePackageManager.java @@ -4,6 +4,7 @@ import android.content.pm.PackageInfo; import android.test.mock.MockPackageManager; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; public class MockInstallablePackageManager extends MockPackageManager { @@ -16,11 +17,36 @@ public class MockInstallablePackageManager extends MockPackageManager { } public void install(String id, int version, String versionName) { - PackageInfo p = new PackageInfo(); - p.packageName = id; - p.versionCode = version; - p.versionName = versionName; - info.add(p); + PackageInfo existing = getPackageInfo(id); + if (existing != null) { + existing.versionCode = version; + existing.versionName = versionName; + } else { + PackageInfo p = new PackageInfo(); + p.packageName = id; + p.versionCode = version; + p.versionName = versionName; + info.add(p); + } + } + + public PackageInfo getPackageInfo(String id) { + for (PackageInfo i : info) { + if (i.packageName.equals(id)) { + return i; + } + } + return null; + } + + public void remove(String id) { + for (Iterator it = info.iterator(); it.hasNext();) { + PackageInfo info = it.next(); + if (info.packageName.equals(id)) { + it.remove(); + return; + } + } } } diff --git a/test/src/org/fdroid/fdroid/AppProviderTest.java b/test/src/org/fdroid/fdroid/AppProviderTest.java index e36a32ba3..12124848e 100644 --- a/test/src/org/fdroid/fdroid/AppProviderTest.java +++ b/test/src/org/fdroid/fdroid/AppProviderTest.java @@ -4,14 +4,13 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.content.res.Resources; import android.database.Cursor; - import mock.MockCategoryResources; import mock.MockContextSwappableComponents; import mock.MockInstallablePackageManager; - import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.InstalledAppCacheUpdater; import java.util.ArrayList; import java.util.List; @@ -41,6 +40,42 @@ public class AppProviderTest extends FDroidProviderTest { }; } + /** + * Although this doesn't directly relate to the AppProvider, it is here because + * the AppProvider used to stumble across this bug when asking for installed apps, + * and the device had over 1000 apps installed. + */ + public void testMaxSqliteParams() { + + MockInstallablePackageManager pm = new MockInstallablePackageManager(); + getSwappableContext().setPackageManager(pm); + + insertApp("com.example.app1", "App 1"); + insertApp("com.example.app100", "App 100"); + insertApp("com.example.app1000", "App 1000"); + + for (int i = 0; i < 50; i ++) { + pm.install("com.example.app" + i, 1, "v" + 1); + } + InstalledAppCacheUpdater.updateInForeground(getMockContext()); + + assertResultCount(1, AppProvider.getInstalledUri()); + + for (int i = 50; i < 500; i ++) { + pm.install("com.example.app" + i, 1, "v" + 1); + } + InstalledAppCacheUpdater.updateInForeground(getMockContext()); + + assertResultCount(2, AppProvider.getInstalledUri()); + + for (int i = 500; i < 1100; i ++) { + pm.install("com.example.app" + i, 1, "v" + 1); + } + InstalledAppCacheUpdater.updateInForeground(getMockContext()); + + assertResultCount(3, AppProvider.getInstalledUri()); + } + public void testCantFindApp() { assertNull(AppProvider.Helper.findById(getMockContentResolver(), "com.example.doesnt-exist")); } @@ -87,7 +122,7 @@ public class AppProviderTest extends FDroidProviderTest { values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreVercode); insertApp(id, "App: " + id, values); - packageManager.install(id, installedVercode, "v" + installedVercode); + TestUtils.installAndBroadcast(getMockContext(), packageManager, id, installedVercode, "v" + installedVercode); } public void testCanUpdate() { @@ -112,7 +147,7 @@ public class AppProviderTest extends FDroidProviderTest { // Can't "update", although can "install"... App notInstalled = AppProvider.Helper.findById(r, "not installed"); - assertFalse(notInstalled.canAndWantToUpdate(c)); + assertFalse(notInstalled.canAndWantToUpdate()); App installedOnlyOneVersionAvailable = AppProvider.Helper.findById(r, "installed, only one version available"); App installedAlreadyLatestNoIgnore = AppProvider.Helper.findById(r, "installed, already latest, no ignore"); @@ -120,21 +155,36 @@ public class AppProviderTest extends FDroidProviderTest { App installedAlreadyLatestIgnoreLatest = AppProvider.Helper.findById(r, "installed, already latest, ignore latest"); App installedAlreadyLatestIgnoreOld = AppProvider.Helper.findById(r, "installed, already latest, ignore old"); - assertFalse(installedOnlyOneVersionAvailable.canAndWantToUpdate(c)); - assertFalse(installedAlreadyLatestNoIgnore.canAndWantToUpdate(c)); - assertFalse(installedAlreadyLatestIgnoreAll.canAndWantToUpdate(c)); - assertFalse(installedAlreadyLatestIgnoreLatest.canAndWantToUpdate(c)); - assertFalse(installedAlreadyLatestIgnoreOld.canAndWantToUpdate(c)); + assertFalse(installedOnlyOneVersionAvailable.canAndWantToUpdate()); + assertFalse(installedAlreadyLatestNoIgnore.canAndWantToUpdate()); + assertFalse(installedAlreadyLatestIgnoreAll.canAndWantToUpdate()); + assertFalse(installedAlreadyLatestIgnoreLatest.canAndWantToUpdate()); + assertFalse(installedAlreadyLatestIgnoreOld.canAndWantToUpdate()); App installedOldNoIgnore = AppProvider.Helper.findById(r, "installed, old version, no ignore"); App installedOldIgnoreAll = AppProvider.Helper.findById(r, "installed, old version, ignore all"); App installedOldIgnoreLatest = AppProvider.Helper.findById(r, "installed, old version, ignore latest"); App installedOldIgnoreNewerNotLatest = AppProvider.Helper.findById(r, "installed, old version, ignore newer, but not latest"); - assertTrue(installedOldNoIgnore.canAndWantToUpdate(c)); - assertFalse(installedOldIgnoreAll.canAndWantToUpdate(c)); - assertFalse(installedOldIgnoreLatest.canAndWantToUpdate(c)); - assertTrue(installedOldIgnoreNewerNotLatest.canAndWantToUpdate(c)); + assertTrue(installedOldNoIgnore.canAndWantToUpdate()); + assertFalse(installedOldIgnoreAll.canAndWantToUpdate()); + assertFalse(installedOldIgnoreLatest.canAndWantToUpdate()); + assertTrue(installedOldIgnoreNewerNotLatest.canAndWantToUpdate()); + + Cursor canUpdateCursor = r.query(AppProvider.getCanUpdateUri(), AppProvider.DataColumns.ALL, null, null, null); + canUpdateCursor.moveToFirst(); + List canUpdateIds = new ArrayList(canUpdateCursor.getCount()); + while (!canUpdateCursor.isAfterLast()) { + canUpdateIds.add(new App(canUpdateCursor).id); + canUpdateCursor.moveToNext(); + } + + String[] expectedUpdateableIds = { + "installed, old version, no ignore", + "installed, old version, ignore newer, but not latest", + }; + + TestUtils.assertContainsOnly(expectedUpdateableIds, canUpdateIds); } public void testIgnored() { @@ -182,9 +232,6 @@ public class AppProviderTest extends FDroidProviderTest { } public void testInstalled() { - - Utils.clearInstalledApksCache(); - MockInstallablePackageManager pm = new MockInstallablePackageManager(); getSwappableContext().setPackageManager(pm); @@ -194,7 +241,7 @@ public class AppProviderTest extends FDroidProviderTest { assertResultCount(0, AppProvider.getInstalledUri()); for (int i = 10; i < 20; i ++) { - pm.install("com.example.test." + i, i, "v1"); + TestUtils.installAndBroadcast(getMockContext(), pm, "com.example.test." + i, i, "v1"); } assertResultCount(10, AppProvider.getInstalledUri()); @@ -237,6 +284,13 @@ public class AppProviderTest extends FDroidProviderTest { return getMockContentResolver().query(AppProvider.getContentUri(), getMinimalProjection(), null, null, null); } + // ======================================================================== + // "Categories" + // (at this point) not an additional table, but we treat them sort of + // like they are. That means that if we change the implementation to + // use a separate table in the future, these should still pass. + // ======================================================================== + public void testCategoriesSingle() { insertAppWithCategory("com.dog", "Dog", "Animal"); insertAppWithCategory("com.rock", "Rock", "Mineral"); @@ -300,12 +354,16 @@ public class AppProviderTest extends FDroidProviderTest { TestUtils.assertContainsOnly(categoriesLonger, expectedLonger); } + // ======================================================================= + // Misc helper functions + // (to be used by any tests in this suite) + // ======================================================================= + private void insertApp(String id, String name) { insertApp(id, name, new ContentValues()); } - private void insertAppWithCategory(String id, String name, - String categories) { + private void insertAppWithCategory(String id, String name, String categories) { ContentValues values = new ContentValues(1); values.put(AppProvider.DataColumns.CATEGORIES, categories); insertApp(id, name, values); diff --git a/test/src/org/fdroid/fdroid/FDroidProviderTest.java b/test/src/org/fdroid/fdroid/FDroidProviderTest.java index a2b4a8a18..1be7eec3d 100644 --- a/test/src/org/fdroid/fdroid/FDroidProviderTest.java +++ b/test/src/org/fdroid/fdroid/FDroidProviderTest.java @@ -9,17 +9,22 @@ import android.net.Uri; import android.os.Build; import android.provider.ContactsContract; import android.test.ProviderTestCase2MockContext; -import mock.MockCategoryResources; import mock.MockContextEmptyComponents; import mock.MockContextSwappableComponents; import mock.MockFDroidResources; -import org.fdroid.fdroid.data.FDroidProvider; -import org.fdroid.fdroid.mock.MockInstalledApkCache; +import org.fdroid.fdroid.data.*; import java.util.List; public abstract class FDroidProviderTest extends ProviderTestCase2MockContext { + private FDroidProvider[] allProviders = { + new AppProvider(), + new RepoProvider(), + new ApkProvider(), + new InstalledAppProvider(), + }; + private MockContextSwappableComponents swappableContext; public FDroidProviderTest(Class providerClass, String providerAuthority) { @@ -33,7 +38,18 @@ public abstract class FDroidProviderTest extends Provi @Override public void setUp() throws Exception { super.setUp(); - Utils.setupInstalledApkCache(new MockInstalledApkCache()); + + // Instantiate all providers other than the one which was already created by the base class. + // This is because F-Droid providers tend to perform joins onto tables managed by other + // providers, and so we need to be able to insert into those other providers for these + // joins to be tested correctly. + for (FDroidProvider provider : allProviders) { + if (!provider.getName().equals(getProvider().getName())) { + provider.attachInfo(getMockContext(), null); + getMockContentResolver().addProvider(provider.getName(), provider); + } + } + getSwappableContext().setResources(getMockResources()); // The *Provider.Helper.* functions tend to take a Context as their @@ -127,4 +143,26 @@ public abstract class FDroidProviderTest extends Provi assertNotNull(result); assertEquals(expectedCount, result.getCount()); } + + protected void assertIsInstalledVersionInDb(String appId, int versionCode, String versionName) { + Uri uri = InstalledAppProvider.getAppUri(appId); + + String[] projection = { + InstalledAppProvider.DataColumns.APP_ID, + InstalledAppProvider.DataColumns.VERSION_CODE, + InstalledAppProvider.DataColumns.VERSION_NAME, + }; + + Cursor cursor = getMockContentResolver().query(uri, projection, null, null, null); + + assertNotNull(cursor); + assertEquals("App \"" + appId + "\" not installed", 1, cursor.getCount()); + + cursor.moveToFirst(); + + assertEquals(appId, cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID))); + assertEquals(versionCode, cursor.getInt(cursor.getColumnIndex(InstalledAppProvider.DataColumns.VERSION_CODE))); + assertEquals(versionName, cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.VERSION_NAME))); + } + } diff --git a/test/src/org/fdroid/fdroid/InstalledAppCacheTest.java b/test/src/org/fdroid/fdroid/InstalledAppCacheTest.java new file mode 100644 index 000000000..2a849da7f --- /dev/null +++ b/test/src/org/fdroid/fdroid/InstalledAppCacheTest.java @@ -0,0 +1,179 @@ +package org.fdroid.fdroid; + +import android.database.Cursor; +import android.net.Uri; +import mock.MockInstallablePackageManager; +import org.fdroid.fdroid.data.InstalledAppCacheUpdater; +import org.fdroid.fdroid.data.InstalledAppProvider; + +/** + * Tests the ability of the {@link org.fdroid.fdroid.data.InstalledAppCacheUpdater} to stay in sync with + * the {@link android.content.pm.PackageManager}. + * For practical reasons, it extends FDroidProviderTest, although there is also a + * separate test for the InstalledAppProvider which tests the CRUD operations in more detail. + */ +public class InstalledAppCacheTest extends FDroidProviderTest { + + private MockInstallablePackageManager packageManager; + + public InstalledAppCacheTest() { + super(InstalledAppProvider.class, InstalledAppProvider.getAuthority()); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + packageManager = new MockInstallablePackageManager(); + getSwappableContext().setPackageManager(packageManager); + } + + @Override + protected String[] getMinimalProjection() { + return new String[] { + InstalledAppProvider.DataColumns.APP_ID + }; + } + + public void install(String appId, int versionCode, String versionName) { + packageManager.install(appId, versionCode, versionName); + } + + public void remove(String appId) { + packageManager.remove(appId); + } + + public void testFromEmptyCache() { + assertResultCount(0, InstalledAppProvider.getContentUri()); + for (int i = 1; i <= 15; i ++) { + install("com.example.app" + i, 200, "2.0"); + } + InstalledAppCacheUpdater.updateInForeground(getMockContext()); + + String[] expectedInstalledIds = { + "com.example.app1", + "com.example.app2", + "com.example.app3", + "com.example.app4", + "com.example.app5", + "com.example.app6", + "com.example.app7", + "com.example.app8", + "com.example.app9", + "com.example.app10", + "com.example.app11", + "com.example.app12", + "com.example.app13", + "com.example.app14", + "com.example.app15", + }; + + TestUtils.assertContainsOnly(getInstalledAppIdsFromProvider(), expectedInstalledIds); + } + + private String[] getInstalledAppIdsFromProvider() { + Uri uri = InstalledAppProvider.getContentUri(); + String[] projection = { InstalledAppProvider.DataColumns.APP_ID }; + Cursor result = getMockContext().getContentResolver().query(uri, projection, null, null, null); + if (result == null) { + return new String[0]; + } + + String[] installedAppIds = new String[result.getCount()]; + result.moveToFirst(); + int i = 0; + while (!result.isAfterLast()) { + installedAppIds[i] = result.getString(result.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)); + result.moveToNext(); + i ++; + } + result.close(); + return installedAppIds; + } + + public void testAppsAdded() { + assertResultCount(0, InstalledAppProvider.getContentUri()); + + install("com.example.app1", 1, "v1"); + install("com.example.app2", 1, "v1"); + install("com.example.app3", 1, "v1"); + InstalledAppCacheUpdater.updateInForeground(getMockContext()); + + assertResultCount(3, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.app1", 1, "v1"); + assertIsInstalledVersionInDb("com.example.app2", 1, "v1"); + assertIsInstalledVersionInDb("com.example.app3", 1, "v1"); + + install("com.example.app10", 1, "v1"); + install("com.example.app11", 1, "v1"); + install("com.example.app12", 1, "v1"); + InstalledAppCacheUpdater.updateInForeground(getMockContext()); + + assertResultCount(6, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.app10", 1, "v1"); + assertIsInstalledVersionInDb("com.example.app11", 1, "v1"); + assertIsInstalledVersionInDb("com.example.app12", 1, "v1"); + } + + public void testAppsRemoved() { + install("com.example.app1", 1, "v1"); + install("com.example.app2", 1, "v1"); + install("com.example.app3", 1, "v1"); + InstalledAppCacheUpdater.updateInForeground(getMockContext()); + + assertResultCount(3, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.app1", 1, "v1"); + assertIsInstalledVersionInDb("com.example.app2", 1, "v1"); + assertIsInstalledVersionInDb("com.example.app3", 1, "v1"); + + remove("com.example.app2"); + InstalledAppCacheUpdater.updateInForeground(getMockContext()); + + assertResultCount(2, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.app1", 1, "v1"); + assertIsInstalledVersionInDb("com.example.app3", 1, "v1"); + } + + public void testAppsUpdated() { + install("com.example.app1", 1, "v1"); + install("com.example.app2", 1, "v1"); + InstalledAppCacheUpdater.updateInForeground(getMockContext()); + + assertResultCount(2, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.app1", 1, "v1"); + assertIsInstalledVersionInDb("com.example.app2", 1, "v1"); + + install("com.example.app2", 20, "v2.0"); + InstalledAppCacheUpdater.updateInForeground(getMockContext()); + + assertResultCount(2, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.app1", 1, "v1"); + assertIsInstalledVersionInDb("com.example.app2", 20, "v2.0"); + } + + public void testAppsAddedRemovedAndUpdated() { + install("com.example.app1", 1, "v1"); + install("com.example.app2", 1, "v1"); + install("com.example.app3", 1, "v1"); + install("com.example.app4", 1, "v1"); + InstalledAppCacheUpdater.updateInForeground(getMockContext()); + + assertResultCount(4, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.app1", 1, "v1"); + assertIsInstalledVersionInDb("com.example.app2", 1, "v1"); + assertIsInstalledVersionInDb("com.example.app3", 1, "v1"); + assertIsInstalledVersionInDb("com.example.app4", 1, "v1"); + + install("com.example.app1", 13, "v1.3"); + remove("com.example.app2"); + remove("com.example.app3"); + install("com.example.app10", 1, "v1"); + InstalledAppCacheUpdater.updateInForeground(getMockContext()); + + assertResultCount(3, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.app1", 13, "v1.3"); + assertIsInstalledVersionInDb("com.example.app4", 1, "v1"); + assertIsInstalledVersionInDb("com.example.app10", 1, "v1"); + + } + +} diff --git a/test/src/org/fdroid/fdroid/InstalledAppProviderTest.java b/test/src/org/fdroid/fdroid/InstalledAppProviderTest.java new file mode 100644 index 000000000..017ac4e7a --- /dev/null +++ b/test/src/org/fdroid/fdroid/InstalledAppProviderTest.java @@ -0,0 +1,168 @@ +package org.fdroid.fdroid; + +import android.content.ContentValues; +import mock.MockInstallablePackageManager; +import org.fdroid.fdroid.data.ApkProvider; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.InstalledAppProvider; +import org.fdroid.fdroid.data.RepoProvider; + +public class InstalledAppProviderTest extends FDroidProviderTest { + + private MockInstallablePackageManager packageManager; + + public InstalledAppProviderTest() { + super(InstalledAppProvider.class, InstalledAppProvider.getAuthority()); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + packageManager = new MockInstallablePackageManager(); + getSwappableContext().setPackageManager(packageManager); + } + + protected MockInstallablePackageManager getPackageManager() { + return packageManager; + } + + public void testUris() { + assertInvalidUri(InstalledAppProvider.getAuthority()); + assertInvalidUri(RepoProvider.getContentUri()); + assertInvalidUri(AppProvider.getContentUri()); + assertInvalidUri(ApkProvider.getContentUri()); + assertInvalidUri("blah"); + + assertValidUri(InstalledAppProvider.getContentUri()); + assertValidUri(InstalledAppProvider.getAppUri("com.example.com")); + assertValidUri(InstalledAppProvider.getAppUri("blah")); + } + + public void testInsert() { + + assertResultCount(0, InstalledAppProvider.getContentUri()); + + insertInstalledApp("com.example.com1", 1, "v1"); + insertInstalledApp("com.example.com2", 2, "v2"); + insertInstalledApp("com.example.com3", 3, "v3"); + + assertResultCount(3, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.com1", 1, "v1"); + assertIsInstalledVersionInDb("com.example.com2", 2, "v2"); + assertIsInstalledVersionInDb("com.example.com3", 3, "v3"); + } + + public void testUpdate() { + + insertInstalledApp("com.example.app1", 10, "1.0"); + insertInstalledApp("com.example.app2", 10, "1.0"); + + assertResultCount(2, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.app2", 10, "1.0"); + + getMockContentResolver().update( + InstalledAppProvider.getAppUri("com.example.app2"), + createContentValues(11, "1.1"), + null, null + ); + + assertResultCount(2, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.app2", 11, "1.1"); + + } + + public void testDelete() { + + insertInstalledApp("com.example.app1", 10, "1.0"); + insertInstalledApp("com.example.app2", 10, "1.0"); + + assertResultCount(2, InstalledAppProvider.getContentUri()); + + getMockContentResolver().delete(InstalledAppProvider.getAppUri("com.example.app1"), null, null); + + assertResultCount(1, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.app2", 10, "1.0"); + + } + + public void testInsertWithBroadcast() { + + installAndBroadcast("com.example.broadcasted1", 10, "v1.0"); + installAndBroadcast("com.example.broadcasted2", 105, "v1.05"); + + assertResultCount(2, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.broadcasted1", 10, "v1.0"); + assertIsInstalledVersionInDb("com.example.broadcasted2", 105, "v1.05"); + } + + public void testUpdateWithBroadcast() { + + installAndBroadcast("com.example.toUpgrade", 1, "v0.1"); + + assertResultCount(1, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.toUpgrade", 1, "v0.1"); + + upgradeAndBroadcast("com.example.toUpgrade", 2, "v0.2"); + + assertResultCount(1, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.toUpgrade", 2, "v0.2"); + + } + + public void testDeleteWithBroadcast() { + + installAndBroadcast("com.example.toKeep", 1, "v0.1"); + installAndBroadcast("com.example.toDelete", 1, "v0.1"); + + assertResultCount(2, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.toKeep", 1, "v0.1"); + assertIsInstalledVersionInDb("com.example.toDelete", 1, "v0.1"); + + removeAndBroadcast("com.example.toDelete"); + + assertResultCount(1, InstalledAppProvider.getContentUri()); + assertIsInstalledVersionInDb("com.example.toKeep", 1, "v0.1"); + + } + + @Override + protected String[] getMinimalProjection() { + return new String[] { + InstalledAppProvider.DataColumns.APP_ID, + InstalledAppProvider.DataColumns.VERSION_CODE, + InstalledAppProvider.DataColumns.VERSION_NAME, + }; + } + + private ContentValues createContentValues(int versionCode, String versionNumber) { + return createContentValues(null, versionCode, versionNumber); + } + + private ContentValues createContentValues(String appId, int versionCode, String versionNumber) { + ContentValues values = new ContentValues(3); + if (appId != null) { + values.put(InstalledAppProvider.DataColumns.APP_ID, appId); + } + values.put(InstalledAppProvider.DataColumns.VERSION_CODE, versionCode); + values.put(InstalledAppProvider.DataColumns.VERSION_NAME, versionNumber); + return values; + } + + private void insertInstalledApp(String appId, int versionCode, String versionNumber) { + ContentValues values = createContentValues(appId, versionCode, versionNumber); + getMockContentResolver().insert(InstalledAppProvider.getContentUri(), values); + } + + private void removeAndBroadcast(String appId) { + TestUtils.removeAndBroadcast(getMockContext(), getPackageManager(), appId); + } + + private void upgradeAndBroadcast(String appId, int versionCode, String versionName) { + TestUtils.upgradeAndBroadcast(getMockContext(), getPackageManager(), appId, versionCode, versionName); + } + + private void installAndBroadcast(String appId, int versionCode, String versionName) { + TestUtils.installAndBroadcast(getMockContext(), getPackageManager(), appId, versionCode, versionName); + } + +} diff --git a/test/src/org/fdroid/fdroid/TestUtils.java b/test/src/org/fdroid/fdroid/TestUtils.java index 1afc0454c..e4927a3fe 100644 --- a/test/src/org/fdroid/fdroid/TestUtils.java +++ b/test/src/org/fdroid/fdroid/TestUtils.java @@ -3,6 +3,7 @@ package org.fdroid.fdroid; import android.content.*; import android.net.Uri; import junit.framework.AssertionFailedError; +import mock.MockInstallablePackageManager; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.AppProvider; @@ -12,10 +13,22 @@ import java.util.List; public class TestUtils { - public static void assertContainsOnly(List actualList, T[] expectedContains) { - List containsList = new ArrayList(expectedContains.length); - Collections.addAll(containsList, expectedContains); - assertContainsOnly(actualList, containsList); + public static void assertContainsOnly(List actualList, T[] expectedArray) { + List expectedList = new ArrayList(expectedArray.length); + Collections.addAll(expectedList, expectedArray); + assertContainsOnly(actualList, expectedList); + } + + public static void assertContainsOnly(T[] actualArray, List expectedList) { + List actualList = new ArrayList(actualArray.length); + Collections.addAll(actualList, actualArray); + assertContainsOnly(actualList, expectedList); + } + + public static void assertContainsOnly(T[] actualArray, T[] expectedArray) { + List expectedList = new ArrayList(expectedArray.length); + Collections.addAll(expectedList, expectedArray); + assertContainsOnly(actualArray, expectedList); } public static String listToString(List list) { @@ -60,6 +73,10 @@ public class TestUtils { } } + public static void insertApp(ContentResolver resolver, String appId, String name) { + insertApp(resolver, appId, name, new ContentValues()); + } + public static void insertApp(ContentResolver resolver, String id, String name, ContentValues additionalValues) { ContentValues values = new ContentValues(); @@ -106,4 +123,56 @@ public class TestUtils { return providerTest.getMockContentResolver().insert(uri, values); } + + /** + * Will tell {@code pm} that we are installing {@code appId}, and then alert the + * {@link org.fdroid.fdroid.PackageAddedReceiver}. This will in turn update the + * "installed apps" table in the database. + * + * Note: in order for this to work, the {@link AppProviderTest#getSwappableContext()} + * will need to be aware of the package manager that we have passed in. Therefore, + * you will have to have called + * {@link mock.MockContextSwappableComponents#setPackageManager(android.content.pm.PackageManager)} + * on the {@link AppProviderTest#getSwappableContext()} before invoking this method. + */ + public static void installAndBroadcast( + Context context, MockInstallablePackageManager pm, + String appId, int versionCode, String versionName) { + + pm.install(appId, versionCode, versionName); + Intent installIntent = new Intent(Intent.ACTION_PACKAGE_ADDED); + installIntent.setData(Uri.parse("package:" + appId)); + new PackageAddedReceiver().onReceive(context, installIntent); + + } + + /** + * @see org.fdroid.fdroid.TestUtils#installAndBroadcast(android.content.Context context, mock.MockInstallablePackageManager, String, int, String) + */ + public static void upgradeAndBroadcast( + Context context, MockInstallablePackageManager pm, + String appId, int versionCode, String versionName) { + /* + removeAndBroadcast(context, pm, appId); + installAndBroadcast(context, pm, appId, versionCode, versionName); + */ + pm.install(appId, versionCode, versionName); + Intent installIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED); + installIntent.setData(Uri.parse("package:" + appId)); + new PackageUpgradedReceiver().onReceive(context, installIntent); + + } + + /** + * @see org.fdroid.fdroid.TestUtils#installAndBroadcast(android.content.Context context, mock.MockInstallablePackageManager, String, int, String) + */ + public static void removeAndBroadcast(Context context, MockInstallablePackageManager pm, String appId) { + + pm.remove(appId); + Intent installIntent = new Intent(Intent.ACTION_PACKAGE_REMOVED); + installIntent.setData(Uri.parse("package:" + appId)); + new PackageRemovedReceiver().onReceive(context, installIntent); + + } + } diff --git a/test/src/org/fdroid/fdroid/mock/MockInstalledApkCache.java b/test/src/org/fdroid/fdroid/mock/MockInstalledApkCache.java deleted file mode 100644 index acac7557f..000000000 --- a/test/src/org/fdroid/fdroid/mock/MockInstalledApkCache.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.fdroid.fdroid.mock; - -import android.content.Context; -import android.content.pm.PackageInfo; -import org.fdroid.fdroid.Utils; - -import java.util.Map; - -public class MockInstalledApkCache extends Utils.InstalledApkCache { - - @Override - public Map getApks(Context context) { - return buildAppList(context); - } - -} From 87775be76c29c7ffeeb7f1282b1eda0860867866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 20 Apr 2014 12:51:50 +0200 Subject: [PATCH 269/282] Update UIL, adapt to the changes --- extern/UniversalImageLoader | 2 +- src/org/fdroid/fdroid/AppDetails.java | 2 +- src/org/fdroid/fdroid/FDroidApp.java | 1 + src/org/fdroid/fdroid/views/AppListAdapter.java | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/extern/UniversalImageLoader b/extern/UniversalImageLoader index 29811229c..ee50fd1ce 160000 --- a/extern/UniversalImageLoader +++ b/extern/UniversalImageLoader @@ -1 +1 @@ -Subproject commit 29811229c3ba3da390b29353875be2c92f88a789 +Subproject commit ee50fd1ce77d866a89374a5ff0886be6e179feb2 diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index a49fdf973..d4d570471 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -259,7 +259,7 @@ public class AppDetails extends ListActivity { displayImageOptions = new DisplayImageOptions.Builder() .cacheInMemory(true) - .cacheOnDisc(true) + .cacheOnDisk(true) .imageScaleType(ImageScaleType.NONE) .showImageOnLoading(R.drawable.ic_repo_app_default) .showImageForEmptyUri(R.drawable.ic_repo_app_default) diff --git a/src/org/fdroid/fdroid/FDroidApp.java b/src/org/fdroid/fdroid/FDroidApp.java index 66d54a219..4ec114050 100644 --- a/src/org/fdroid/fdroid/FDroidApp.java +++ b/src/org/fdroid/fdroid/FDroidApp.java @@ -126,6 +126,7 @@ public class FDroidApp extends Application { .discCache(new LimitedAgeDiscCache( new File(StorageUtils.getCacheDirectory(getApplicationContext(), true), "icons"), + null, new FileNameGenerator() { @Override public String generate(String imageUri) { diff --git a/src/org/fdroid/fdroid/views/AppListAdapter.java b/src/org/fdroid/fdroid/views/AppListAdapter.java index 8396d5a57..013f4aad4 100644 --- a/src/org/fdroid/fdroid/views/AppListAdapter.java +++ b/src/org/fdroid/fdroid/views/AppListAdapter.java @@ -46,7 +46,7 @@ abstract public class AppListAdapter extends CursorAdapter { Context.LAYOUT_INFLATER_SERVICE); displayImageOptions = new DisplayImageOptions.Builder() .cacheInMemory(true) - .cacheOnDisc(true) + .cacheOnDisk(true) .imageScaleType(ImageScaleType.NONE) .showImageOnLoading(R.drawable.ic_repo_app_default) .showImageForEmptyUri(R.drawable.ic_repo_app_default) From f93c8151fe125bba21923841023290e47515384b Mon Sep 17 00:00:00 2001 From: F-Droid Translatebot Date: Sun, 20 Apr 2014 12:40:49 +0100 Subject: [PATCH 270/282] Translation updates --- res/values-de/strings.xml | 2 +- res/values-it/strings.xml | 8 ++++ res/values-nb/array.xml | 4 +- res/values-nb/strings.xml | 80 ++++++++++++++++++++++++++++++++++++++- res/values-pl/strings.xml | 2 +- res/values-ru/strings.xml | 31 ++++++++------- 6 files changed, 105 insertions(+), 22 deletions(-) diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index f324dd74f..ef5b17981 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -63,7 +63,7 @@ Die Adresse einer Paketquelle könnte wie folgt aussehen: https://f-droid.org/re %d Aktualisierungen sind verfügbar. F-Droid: Aktualisierungen verfügbar Bitte warten - Anwendungsliste wird aktualisiert… + Anwendungsliste wird aktualisiert … Anwendung wird heruntergeladen von NFC ist deaktiviert! NFC-Einstellungen öffnen … diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 89b82eae6..4f4917204 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -9,6 +9,7 @@ Versione Modifica Elimina + Attiva Invio NFC... Cache applicazioni Conserva i file apk scaricati sulla scheda SD Non conservare i file apk @@ -66,6 +67,7 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo NFC non attivato! Vai alle Impostazioni NFC… Nessun metodo d\'invio via Bluetooth trovato, selezionane uno! + Scegli il metodo di invio Bluetooth Indirizzo repository Impronta digitale (opzionale) Questo repository esiste già! @@ -73,10 +75,12 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo Questo repository è già configurato, conferma di volerlo abilitare nuovamente. Il nuovo repository è già configurato e abilitato! È necessario eliminare questo repository prima di poter aggiungerne uno con una chiave differente! + Ignoro URI repo malformati: %s L\'elenco dei repository in uso è cambiato. Vuoi aggiornarlo? Aggiorna i Repository Gestione dei repository + Bluetooth FDroid.apk… Preferenze Informazioni Cerca @@ -122,6 +126,8 @@ Vuoi aggiornarlo? Tutte Novità Aggiornate di Recente + Repo FDroid Locali + Rilevazione repo FDroid locali... Scaricamento %2$s / %3$s (%4$d%%) da %1$s @@ -131,6 +137,7 @@ Vuoi aggiornarlo? Connessione a %1$s Controllo compatibilità applicazioni con il tuo dispositivo… + Salvataggio dettagli applicazioni (%1$d%%) Non viene usata alcuna autorizzazione. Autorizzazioni per la versione %s Mostra autorizzazioni @@ -144,6 +151,7 @@ Vuoi aggiornarlo? Non firmato URL Numero di applicazioni + Impronta digitale della Chiave del Repo (SHA-256) Descrizione Ultimo aggiornamento Aggiorna diff --git a/res/values-nb/array.xml b/res/values-nb/array.xml index bdf6445f2..045ca6468 100644 --- a/res/values-nb/array.xml +++ b/res/values-nb/array.xml @@ -8,7 +8,7 @@ Daglig - Mørk - Lys + Mørkt + Lyst diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index 02159393f..a2ed8c4f6 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -7,14 +7,26 @@ Det ser ut til at denne pakken ikke er kompatibel med ditt utstyr. Vil du prøve å installere det likevel? Du prøver å nedgradere denne applikasjonen. Dette kan føre til at applikasjonen henger, og du kan til og med miste dine data. Vil du prøve å nedgradere likevel? Versjon + Rediger + Slett + Skru på NFC-sending Lagre nedlastede applikasjoner i buffer + Behold nedlastede apk-filer på minnekortet + Ikke behold noen apk-filer Oppdateringer Andre Forrige registeroppdatering: %s aldri + Intervall for automatisk oppdatering + Ingen automatiske registerlisteoppdateringer Bare på trådløst + Oppdater applikasjoner automatisk kun på trådløst + Alltid oppdater applikasjonsliste automatisk Varsle + Varsle når nye oppdateringer er tilgjengelige + Ikke vis varsel om noen oppdateringer Oppdater historie + Dager nytt innhold skal anses som ferskt: %s Søkeresultater Programdetaljer Program ikke funnet @@ -35,8 +47,11 @@ Lisensiert GNU GPLv3. Legg til nytt register Legg til Avbryt + Skru på + Legg til nøkkel + Overskriv Velg register du vil fjerne - Oppdater registrene + Oppdater registerne Tilgjengelig Oppdateringer 1 oppdatering tilgjengelig. @@ -45,15 +60,28 @@ Lisensiert GNU GPLv3. Vennligst vent Oppdaterer applikasjonsliste… Henter program fra + NFC (nærfeltskommunikasjon) er ikke påskrudd! + Gå til NFC-innstillinger + Ingen forsendelsesmåte for blåtann funnet, velg en! + Velg forsendelsesmåte for Blåtann Registeradresse + Fingeravtrykk (valgfritt) + Dette registret eksisterer allerede! + Dette registret er allerede satt opp, dette vil legge til ny informasjon om nøkler. + Dette registret er allerede satt opp, bekreft at du vil skru det på igjen. + Oppstrøms-registret er allerede oppsatt og skrudd på! + Du må først slette dette registret før du kan legge til et med en en annen nøkkel! + Ignorerer ugyldig registernettadresse: %s Listen over brukte register har endret seg. Vil du oppdatere dem? Oppdater registrene Endre registrene + Blåtann FDroid.apk... Innstillinger Om Søk Nytt register Fjern register + Finn lokale register Kjør Del Installer @@ -61,7 +89,7 @@ Lisensiert GNU GPLv3. Ignorer alle oppdateringer Ignorer denne oppdateringen Nettside - Saker + Problemoversikt Kildekode Oppgrader Doner @@ -74,16 +102,27 @@ Lisensiert GNU GPLv3. Dette programmet promoterer ufrie utvidelser Dette programmet promoterer ufrie nettverkstjenester Dette programmet avhenger av andre ufrie applikasjoner + Koden fra kilden er ikke helt fri Vis Ekspert + Hvis ekstra info og skru på ekstra innstillinger + Gjem ekstra info for erfarne brukere Søk i programliste Programstøtte Ukompatible versjoner + Vis programvareversjoner som er ukompatible med enheten + Gjem programvareversjoner som er ukompatible med enheten Rot + Skyggelegg alle app-er som krever superbruker-tilgang + Skyggelegg alle app-er som etterspør superbruker-rettigheter Ignorer pekeskjerm + Alltid inkluder app-er som krever pekeskjerm + Filtrer app-er som normalt Alle Det som er nytt Nylig oppdatert + Lokale F-Droid register + Oppdager lokale FDroid register... Laster ned %2$s / %3$s (%4$d%%) fra %1$s @@ -93,10 +132,47 @@ Lisensiert GNU GPLv3. Kobler til %1$s Sjekker programstøtte for ditt utstyr… + Lagrer programdata (%1$d%%) Krever ingen tillatelser. Tillatelser for versjon %s Vis tillatelser + Vis en liste over rettighetene en app krever + Ikke vis rettigheter før nedlasting Du har ingen tilgjengelige applikasjoner som kan håndtere %s Kompakt layout + Vis ikoner i mindre størrelse + Vis ikoner i vanlig størrelse Utseende + Usignert + URL + Antall app-er + Fingeravtrykk for signeringsnøkkel til register (SHA-256) + Beskrivelse + Siste oppdatering + Oppdater + Navn + Dette betyr at listen over applikasjoner ikke er kontrollert. Du bør være forsiktig med applikasjoner lastet ned fra usignerte lister. + Dette registret har ikke blitt brukt enda. +For å se app-ene det har tilgjengelig, må du oppdatere det. + +Når så er gjort, vil beskrivelser og andre detaljer bli tilgjengelige her. + Vil du slette \"{0}\" +registret, som har {1} app-er i seg? Alle installerte app-er vil IKKE bli +fjernet, men det kan ikke oppdatere dem med F-droid lengre. + Ukjent + Slett register? + Sletting av register betyr + at applikasjoner derfra ikke lenger vil være tilgjengelige i F-droid. + +Merk: Alle + tidligere installerte apper vil fremdeles være å finne på din enhet. + Avskrudd \"%1$\". + +Du må + skru på dette registeret igjen for å installere applikasjoner fra det. + %s eller senere + opptil %s + %1$s opptil %2$s + Din enhet er ikke på det samme lokale WiFi-nettet som registret du nettopp la til! Prøv å koble til dette nettverket: %s + Krever: %1$s diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 6e709bf01..fc1cc4a16 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -150,7 +150,7 @@ Opis i inne szczegóły będą tu dostępne gdy zaktualizujesz repozytoriumUsunięcie repozytorium oznacza, że aplikacje z niego nie będą dłużej dostępne w F-Droid. Uwaga: Wszystkie poprzednio zainstalowane aplikacje zostaną na urządzeniu. - Zablokowane \"%1$s\". + Zablokowane \"%1%s\". Aby zainstalować aplikacje z tego repozytorium musisz je włączyć ponownie. %s lub później diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index d67c37962..9250d4172 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -9,6 +9,7 @@ Версия Редактировать Удалить + Включить отправку по NFC… Кэшировать загруженные приложения Хранить загруженные файлы apk на SD-карте Не хранить файлы apk @@ -63,6 +64,10 @@ Подождите Список приложений обновляется… Взять приложение из + NFC не включен! + Перейти в Настройки NFC… + Ни один метод отправки по Bluetooth не найден, выберите один из них! + Выберите метод отправки по Bluetooth Адрес репозитория Отпечаток ключа (опционально) Этот репозиторий уже существует! @@ -70,15 +75,18 @@ Этот репозиторий уже настроен, убедитесь, что вы хотите снова включить его. Репозиторий уже настроен и включен! Вы должны удалить этот репозиторий, прежде чем вы можете добавить новый с другим ключом! + Игнорирование неверного URI репозитория: %s Список репозиториев изменился. Обновить его? Обновить репозитории Репозитории + Bluetooth FDroid.apk… Настройки О программе Поиск Новый репозиторий Удалить репозиторий + Найти локальные репозитории Запустить Поделиться Установить @@ -118,6 +126,8 @@ Все Что Нового Недавно обновлённые + Локальные репозитории FDroid + Обнаружение локальных репозиториев FDroid… Загрузка %2$s / %3$s (%4$d%%) из %1$s @@ -127,6 +137,7 @@ Соединение с %1$s Проверка совместимости приложений с вашим устройством… + Сохранение данных приложений (%1$d%%) Разрешений не требуется. Разрешения для версии %s Показывать разрешения @@ -140,6 +151,7 @@ Неподписанный URL Количество приложений + Отпечаток ключа подписи репозитория (SHA-256) Описание Последние обновление Обновить @@ -155,20 +167,7 @@ \"%1$s\" отключен. Вам нужно повторно включить этот репозиторий для установки приложений из него. %s или позднее до %s - - Детские - Разработка - Игры - Интернет - Математика - Мультимедиа - Навигация - Новости - Офис - Связь - Чтение - Научные - Безопасность - Системные - Обои + %1$s до %2$s + Данное устройство не находится в той же сети Wi-Fi, что и локальный репозиторий, который вы только что добавили! Попробуйте присоединиться к этой сети: %s + Требуется: %1$s From c5a1fd9b1c774aa7ca965d6ceb047e7e0c57a66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 20 Apr 2014 14:24:07 +0200 Subject: [PATCH 271/282] Some translation fixes --- res/values-it/strings.xml | 4 ++-- res/values-nb/strings.xml | 4 ++-- res/values-pl/strings.xml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 4f4917204..8b3835d67 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -9,7 +9,7 @@ Versione Modifica Elimina - Attiva Invio NFC... + Attiva Invio NFC… Cache applicazioni Conserva i file apk scaricati sulla scheda SD Non conservare i file apk @@ -127,7 +127,7 @@ Vuoi aggiornarlo? Novità Aggiornate di Recente Repo FDroid Locali - Rilevazione repo FDroid locali... + Rilevazione repo FDroid locali… Scaricamento %2$s / %3$s (%4$d%%) da %1$s diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index a2ed8c4f6..d6baa646c 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -75,7 +75,7 @@ Lisensiert GNU GPLv3. Listen over brukte register har endret seg. Vil du oppdatere dem? Oppdater registrene Endre registrene - Blåtann FDroid.apk... + Blåtann FDroid.apk… Innstillinger Om Søk @@ -122,7 +122,7 @@ Lisensiert GNU GPLv3. Det som er nytt Nylig oppdatert Lokale F-Droid register - Oppdager lokale FDroid register... + Oppdager lokale FDroid register… Laster ned %2$s / %3$s (%4$d%%) fra %1$s diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index fc1cc4a16..6e709bf01 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -150,7 +150,7 @@ Opis i inne szczegóły będą tu dostępne gdy zaktualizujesz repozytoriumUsunięcie repozytorium oznacza, że aplikacje z niego nie będą dłużej dostępne w F-Droid. Uwaga: Wszystkie poprzednio zainstalowane aplikacje zostaną na urządzeniu. - Zablokowane \"%1%s\". + Zablokowane \"%1$s\". Aby zainstalować aplikacje z tego repozytorium musisz je włączyć ponownie. %s lub później From 4b3b392c01d650ab7ba88c448a87bee677613ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 20 Apr 2014 14:32:05 +0200 Subject: [PATCH 272/282] Prepare for 0.64-test --- AndroidManifest.xml | 4 ++-- CHANGELOG.md | 14 ++++++++++++++ res/values/no_trans.xml | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 79160e0db..ae0be10db 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="640" + android:versionName="0.64-test" > F-Droid - 0.63 + 0.64-test https://f-droid.org team@f-droid.org From fe41133d2b383ed08ada3216fcbc0071128d017f Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Mon, 21 Apr 2014 09:20:27 +0000 Subject: [PATCH 273/282] Modified changelog details for 'installed app cache' Made the description a little more appropriate for lay people, rather than developers. --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 633e5cb7e..3018a0243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,15 @@ ### Upcoming release -* Cache the installed apps in the database for better performance and - usability, but mainly to fix the sqlite crashes at startup caused by the - sqlite limit of parameters in queries +* Fix crash on startup for devices with more than 500 installed apps + +* Improved performance for devices with many installed apps * Improve ellipsizing and spacing in the app lists * Start translating the category lists +* Keep track of installed apps internally, rather than asking Android each time + * Fix some crashes * Translation updates From 52e0f373af74369b6de416936d087cf0ac8cdb3c Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 8 Apr 2014 14:32:50 -0400 Subject: [PATCH 274/282] stay in FDroid after adding a new repo via Intent If a new repo comes in via Intent, like from clicking a link, scanning a QR Code, etc., then stay in FDroid once the add dialog is complete. Previously, it would sometimes stay in FDroid and sometimes go back to the sending Activity, depending on the sending Activity. It was confusing and annoying behavior. --- AndroidManifest.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ae0be10db..75c1ce9ec 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -96,9 +96,8 @@ Date: Sun, 20 Apr 2014 21:58:08 -0400 Subject: [PATCH 275/282] enable sending installed APKs via NFC/Android Beam on AppDetails If you are viewing the AppDetails screen for an installed app, this code configures Android Beam to send the APK for that installed app if the you initiate via NFC. Also move the SDK checks into each method so that they are easier to use without doing the wrong thing. --- src/org/fdroid/fdroid/AppDetails.java | 12 ++++-- src/org/fdroid/fdroid/FDroid.java | 24 +---------- src/org/fdroid/fdroid/NfcBeamManager.java | 43 +++++++++++++++++++ .../fdroid/views/RepoDetailsActivity.java | 13 +++--- 4 files changed, 60 insertions(+), 32 deletions(-) create mode 100644 src/org/fdroid/fdroid/NfcBeamManager.java diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index d4d570471..51e7fdecb 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -34,11 +34,13 @@ import android.app.AlertDialog; import android.app.ListActivity; import android.app.ProgressDialog; import android.net.Uri; +import android.nfc.NfcAdapter; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.preference.PreferenceManager; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageInfo; import android.content.pm.Signature; @@ -624,11 +626,14 @@ public class AppDetails extends ListActivity { adapter.notifyDataSetChanged(); TextView tv = (TextView) findViewById(R.id.status); - if (!app.isInstalled()) - tv.setText(getString(R.string.details_notinstalled)); - else + if (app.isInstalled()) { tv.setText(getString(R.string.details_installed, app.installedVersionName)); + NfcBeamManager.setAndroidBeam(this, app.id); + } else { + tv.setText(getString(R.string.details_notinstalled)); + NfcBeamManager.disableAndroidBeam(this); + } tv = (TextView) infoView.findViewById(R.id.signature); if (pref_expert && mInstalledSignature != null) { @@ -1101,5 +1106,4 @@ public class AppDetails extends ListActivity { break; } } - } diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index 8dc53721d..f4e912fa2 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -129,9 +129,8 @@ public class FDroid extends FragmentActivity { @Override protected void onResume() { super.onResume(); - // RepoDetailsActivity sets a different beam, so reset here - if (Build.VERSION.SDK_INT >= 16) - setupAndroidBeam(); + // AppDetails and RepoDetailsActivity set different NFC actions, so reset here + NfcBeamManager.setAndroidBeam(this, getApplication().getPackageName()); } @Override @@ -422,23 +421,4 @@ public class FDroid extends FragmentActivity { } - @TargetApi(16) - private void setupAndroidBeam() { - PackageManager pm = getPackageManager(); - NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); - if (nfcAdapter != null) { - ApplicationInfo appInfo; - try { - appInfo = pm.getApplicationInfo("org.fdroid.fdroid", - PackageManager.GET_META_DATA); - // TODO can we send the repo here also, as a file? - Uri uris[] = { - Uri.parse("file://" + appInfo.publicSourceDir), - }; - nfcAdapter.setBeamPushUris(uris, this); - } catch (NameNotFoundException e1) { - e1.printStackTrace(); - } - } - } } diff --git a/src/org/fdroid/fdroid/NfcBeamManager.java b/src/org/fdroid/fdroid/NfcBeamManager.java new file mode 100644 index 000000000..488fe7d67 --- /dev/null +++ b/src/org/fdroid/fdroid/NfcBeamManager.java @@ -0,0 +1,43 @@ + +package org.fdroid.fdroid; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.nfc.NfcAdapter; +import android.os.Build; + +@TargetApi(16) +public class NfcBeamManager { + + static void setAndroidBeam(Activity activity, String packageName) { + if (Build.VERSION.SDK_INT < 16) + return; + PackageManager pm = activity.getPackageManager(); + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(activity); + if (nfcAdapter != null) { + ApplicationInfo appInfo; + try { + appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); + Uri uris[] = { + Uri.parse("file://" + appInfo.publicSourceDir), + }; + nfcAdapter.setBeamPushUris(uris, activity); + } catch (NameNotFoundException e) { + e.printStackTrace(); + } + } + } + + static void disableAndroidBeam(Activity activity) { + if (Build.VERSION.SDK_INT < 16) + return; + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(activity); + if (nfcAdapter != null) + nfcAdapter.setBeamPushUris(null, activity); + } + +} diff --git a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java index 268557372..822975b01 100644 --- a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -69,14 +69,12 @@ public class RepoDetailsActivity extends FragmentActivity { setTitle(repo.getName()); wifiManager = (WifiManager) getSystemService(WIFI_SERVICE); - - // required NFC support starts in android-14 - if (Build.VERSION.SDK_INT >= 14) - setNfc(); } @TargetApi(14) private void setNfc() { + if (Build.VERSION.SDK_INT < 14) + return; NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); if (nfcAdapter == null) { return; @@ -97,8 +95,9 @@ public class RepoDetailsActivity extends FragmentActivity { public void onResume() { Log.i(TAG, "onResume"); super.onResume(); - if (Build.VERSION.SDK_INT >= 9) - processIntent(getIntent()); + // FDroid.java and AppDetails set different NFC actions, so reset here + setNfc(); + processIntent(getIntent()); } @Override @@ -112,6 +111,8 @@ public class RepoDetailsActivity extends FragmentActivity { @TargetApi(9) void processIntent(Intent i) { + if (Build.VERSION.SDK_INT < 9) + return; if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(i.getAction())) { Log.i(TAG, "ACTION_NDEF_DISCOVERED"); Parcelable[] rawMsgs = From 4a55cdf9380fc77b51621e53690b53a63036ff09 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 21 Apr 2014 21:13:34 -0400 Subject: [PATCH 276/282] option to send via bluetooth any installed app on the AppDetails page This takes the code used for sending the FDroid.apk and applies it to any installed app. So the user can go to the AppDetails for any installed app and select "Send via Bluetooth" from the menu, and send the app to another phone. --- res/values/strings.xml | 1 + src/org/fdroid/fdroid/AppDetails.java | 49 +++++++++++++------ src/org/fdroid/fdroid/FDroid.java | 64 ++----------------------- src/org/fdroid/fdroid/FDroidApp.java | 68 +++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 73 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 0d0861e7c..b068d0ae9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -78,6 +78,7 @@ Go to NFC Settings… No Bluetooth send method found, choose one! Choose Bluetooth send method + Send via Bluetooth Repository address Fingerprint (optional) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 51e7fdecb..1e40c9ba1 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -19,11 +19,6 @@ package org.fdroid.fdroid; -import java.io.File; -import java.security.NoSuchAlgorithmException; -import java.util.Iterator; -import java.util.List; - import android.content.*; import android.widget.*; import org.fdroid.fdroid.data.*; @@ -33,6 +28,7 @@ import android.annotation.TargetApi; import android.app.AlertDialog; import android.app.ListActivity; import android.app.ProgressDialog; +import android.bluetooth.BluetoothAdapter; import android.net.Uri; import android.nfc.NfcAdapter; import android.os.Build; @@ -40,6 +36,8 @@ import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.preference.PreferenceManager; +import android.support.v4.app.NavUtils; +import android.support.v4.view.MenuItemCompat; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageInfo; @@ -60,26 +58,31 @@ import android.view.View; import android.view.ViewGroup; import android.graphics.Bitmap; -import android.support.v4.app.NavUtils; -import android.support.v4.view.MenuItemCompat; - -import org.fdroid.fdroid.compat.PackageManagerCompat; -import org.fdroid.fdroid.compat.ActionBarCompat; -import org.fdroid.fdroid.compat.MenuManager; -import org.fdroid.fdroid.Utils.CommaSeparatedList; - import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.ImageScaleType; +import org.fdroid.fdroid.Utils.CommaSeparatedList; +import org.fdroid.fdroid.compat.ActionBarCompat; +import org.fdroid.fdroid.compat.MenuManager; +import org.fdroid.fdroid.compat.PackageManagerCompat; + +import java.io.File; +import java.security.NoSuchAlgorithmException; +import java.util.Iterator; +import java.util.List; + public class AppDetails extends ListActivity { + private static final String TAG = "AppDetails"; private static final int REQUEST_INSTALL = 0; private static final int REQUEST_UNINSTALL = 1; + public static final int REQUEST_ENABLE_BLUETOOTH = 2; public static final String EXTRA_APPID = "appid"; public static final String EXTRA_FROM = "from"; + private FDroidApp fdroidApp; private ApkListAdapter adapter; private static class ViewHolder { @@ -236,6 +239,7 @@ public class AppDetails extends ListActivity { private static final int DOGECOIN = Menu.FIRST + 12; private static final int FLATTR = Menu.FIRST + 13; private static final int DONATE_URL = Menu.FIRST + 14; + private static final int SEND_VIA_BLUETOOTH = Menu.FIRST + 15; private App app; private String appid; @@ -255,7 +259,8 @@ public class AppDetails extends ListActivity { @Override protected void onCreate(Bundle savedInstanceState) { - ((FDroidApp) getApplication()).applyTheme(this); + fdroidApp = ((FDroidApp) getApplication()); + fdroidApp.applyTheme(this); super.onCreate(savedInstanceState); @@ -760,6 +765,9 @@ public class AppDetails extends ListActivity { if (app.donateURL != null) donate.add(Menu.NONE, DONATE_URL, 10, R.string.menu_website); } + if (app.isInstalled() && fdroidApp.bluetoothAdapter != null) { // ignore on devices without Bluetooth + menu.add(Menu.NONE, SEND_VIA_BLUETOOTH, 6, R.string.send_via_bluetooth); + } return true; } @@ -850,6 +858,17 @@ public class AppDetails extends ListActivity { tryOpenUri(app.donateURL); return true; + case SEND_VIA_BLUETOOTH: + /* + * If Bluetooth has not been enabled/turned on, then + * enabling device discoverability will automatically enable Bluetooth + */ + Intent discoverBt = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); + discoverBt.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 121); + startActivityForResult(discoverBt, REQUEST_ENABLE_BLUETOOTH); + // if this is successful, the Bluetooth transfer is started + return true; + } return super.onOptionsItemSelected(item); } @@ -1104,6 +1123,8 @@ public class AppDetails extends ListActivity { case REQUEST_UNINSTALL: resetRequired = true; break; + case REQUEST_ENABLE_BLUETOOTH: + fdroidApp.sendViaBluetooth(this, resultCode, app.id); } } } diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index f4e912fa2..48b87e76d 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -20,18 +20,15 @@ package org.fdroid.fdroid; import android.annotation.TargetApi; -import android.app.Activity; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.NotificationManager; import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothManager; import android.content.*; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.database.ContentObserver; import android.net.Uri; @@ -48,7 +45,6 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.TextView; -import android.widget.Toast; import org.fdroid.fdroid.compat.TabManager; import org.fdroid.fdroid.data.AppProvider; @@ -70,8 +66,7 @@ public class FDroid extends FragmentActivity { private static final int SEARCH = Menu.FIRST + 4; private static final int BLUETOOTH_APK = Menu.FIRST + 5; - /* request codes for Bluetooth flows */ - private BluetoothAdapter mBluetoothAdapter = null; + private FDroidApp fdroidApp = null; private ViewPager viewPager; @@ -80,7 +75,8 @@ public class FDroid extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { - ((FDroidApp) getApplication()).applyTheme(this); + fdroidApp = ((FDroidApp) getApplication()); + fdroidApp.applyTheme(this); super.onCreate(savedInstanceState); setContentView(R.layout.fdroid); @@ -112,18 +108,6 @@ public class FDroid extends FragmentActivity { Uri uri = AppProvider.getContentUri(); getContentResolver().registerContentObserver(uri, true, new AppObserver()); - - getBluetoothAdapter(); - } - - @TargetApi(18) - private void getBluetoothAdapter() { - // to use the new, recommended way of getting the adapter - // http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html - if (Build.VERSION.SDK_INT < 18) - mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - else - mBluetoothAdapter = ((BluetoothManager) getSystemService(BLUETOOTH_SERVICE)).getAdapter(); } @Override @@ -149,7 +133,7 @@ public class FDroid extends FragmentActivity { android.R.drawable.ic_menu_agenda); MenuItem search = menu.add(Menu.NONE, SEARCH, 3, R.string.menu_search).setIcon( android.R.drawable.ic_menu_search); - if (mBluetoothAdapter != null) // ignore on devices without Bluetooth + if (fdroidApp.bluetoothAdapter != null) // ignore on devices without Bluetooth menu.add(Menu.NONE, BLUETOOTH_APK, 3, R.string.menu_send_apk_bt); menu.add(Menu.NONE, PREFERENCES, 4, R.string.menu_preferences).setIcon( android.R.drawable.ic_menu_preferences); @@ -298,45 +282,7 @@ public class FDroid extends FragmentActivity { } break; case REQUEST_ENABLE_BLUETOOTH: - if (resultCode == Activity.RESULT_CANCELED) - break; - String packageName = null; - String className = null; - boolean found = false; - Intent sendBt = null; - try { - PackageManager pm = getPackageManager(); - ApplicationInfo appInfo = pm.getApplicationInfo("org.fdroid.fdroid", - PackageManager.GET_META_DATA); - sendBt = new Intent(Intent.ACTION_SEND); - // The APK type is blocked by stock Android, so use zip - // sendBt.setType("application/vnd.android.package-archive"); - sendBt.setType("application/zip"); - sendBt.putExtra(Intent.EXTRA_STREAM, - Uri.parse("file://" + appInfo.publicSourceDir)); - // not all devices have the same Bluetooth Activities, so - // let's find it - for (ResolveInfo info : pm.queryIntentActivities(sendBt, 0)) { - packageName = info.activityInfo.packageName; - if (packageName.equals("com.android.bluetooth") - || packageName.equals("com.mediatek.bluetooth")) { - className = info.activityInfo.name; - found = true; - break; - } - } - } catch (NameNotFoundException e1) { - e1.printStackTrace(); - found = false; - } - if (!found) { - Toast.makeText(this, R.string.bluetooth_activity_not_found, - Toast.LENGTH_SHORT).show(); - startActivity(Intent.createChooser(sendBt, getString(R.string.choose_bt_send))); - } else { - sendBt.setClassName(packageName, className); - startActivity(sendBt); - } + fdroidApp.sendViaBluetooth(this, resultCode, "org.fdroid.fdroid"); break; } } diff --git a/src/org/fdroid/fdroid/FDroidApp.java b/src/org/fdroid/fdroid/FDroidApp.java index 4ec114050..b722a1221 100644 --- a/src/org/fdroid/fdroid/FDroidApp.java +++ b/src/org/fdroid/fdroid/FDroidApp.java @@ -18,17 +18,31 @@ package org.fdroid.fdroid; +import android.annotation.TargetApi; import android.app.Activity; import android.app.Application; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothManager; +import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Build; import android.preference.PreferenceManager; import android.util.Log; +import android.widget.Toast; + import com.nostra13.universalimageloader.cache.disc.impl.LimitedAgeDiscCache; import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; import com.nostra13.universalimageloader.utils.StorageUtils; + import de.duenndns.ssl.MemorizingTrustManager; + import org.fdroid.fdroid.compat.PRNGFixes; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.InstalledAppCacheUpdater; @@ -44,6 +58,8 @@ import java.security.NoSuchAlgorithmException; public class FDroidApp extends Application { + BluetoothAdapter bluetoothAdapter = null; + private static enum Theme { dark, light } @@ -121,6 +137,7 @@ public class FDroidApp extends Application { } UpdateService.schedule(getApplicationContext()); + bluetoothAdapter = getBluetoothAdapter(); ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(getApplicationContext()) .discCache(new LimitedAgeDiscCache( @@ -179,4 +196,55 @@ public class FDroidApp extends Application { } } + @TargetApi(18) + private BluetoothAdapter getBluetoothAdapter() { + // to use the new, recommended way of getting the adapter + // http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html + if (Build.VERSION.SDK_INT < 18) + return BluetoothAdapter.getDefaultAdapter(); + else + return ((BluetoothManager) getSystemService(BLUETOOTH_SERVICE)).getAdapter(); + } + + void sendViaBluetooth(Activity activity, int resultCode, String packageName) { + if (resultCode == Activity.RESULT_CANCELED) + return; + String bluetoothPackageName = null; + String className = null; + boolean found = false; + Intent sendBt = null; + try { + PackageManager pm = getPackageManager(); + ApplicationInfo appInfo = pm.getApplicationInfo(packageName, + PackageManager.GET_META_DATA); + sendBt = new Intent(Intent.ACTION_SEND); + // The APK type is blocked by stock Android, so use zip + // sendBt.setType("application/vnd.android.package-archive"); + sendBt.setType("application/zip"); + sendBt.putExtra(Intent.EXTRA_STREAM, + Uri.parse("file://" + appInfo.publicSourceDir)); + // not all devices have the same Bluetooth Activities, so + // let's find it + for (ResolveInfo info : pm.queryIntentActivities(sendBt, 0)) { + bluetoothPackageName = info.activityInfo.packageName; + if (bluetoothPackageName.equals("com.android.bluetooth") + || bluetoothPackageName.equals("com.mediatek.bluetooth")) { + className = info.activityInfo.name; + found = true; + break; + } + } + } catch (NameNotFoundException e1) { + e1.printStackTrace(); + found = false; + } + if (!found) { + Toast.makeText(this, R.string.bluetooth_activity_not_found, + Toast.LENGTH_SHORT).show(); + activity.startActivity(Intent.createChooser(sendBt, getString(R.string.choose_bt_send))); + } else { + sendBt.setClassName(bluetoothPackageName, className); + activity.startActivity(sendBt); + } + } } From d573bac5b02724123739a9cceba1e975ef9e05f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 23 Apr 2014 18:05:23 +0200 Subject: [PATCH 277/282] Add the sharing feature by Hans to the changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3018a0243..6719fbf4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ * Fix crash on startup for devices with more than 500 installed apps +* Send apps to other devices directly from the App Details screen via NFC or Bluetooth + * Improved performance for devices with many installed apps * Improve ellipsizing and spacing in the app lists From 71db322b6d7a800d563b77578ce4cdc3a5eb2361 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 24 Apr 2014 07:58:19 +0930 Subject: [PATCH 278/282] Don't implement 'update' for installed apps, use replace (Fixes #14) There were some weird edge cases that couldn't quite be pinned down, whereby installing an app would result in a unique key violation being hit. One example was when somebody was installing an apk from a file manager. It seems that this doesn't trigger a PACKAGE_CHANGED, but rather a PACKAGE_INSTALLED. The end result is that it attempts to insert a record that already exists in the installed apps table. Because we have a unique key constraing on the appId, it breaks. This commit changes the way that we insert installed app details. Instead of inserting some times, and updating other times, we always insert. If we hit a unique key violation, the row is deleted, and then the new values are reinserted. --- .../fdroid/PackageUpgradedReceiver.java | 5 ++-- .../fdroid/data/InstalledAppCacheUpdater.java | 28 ++----------------- .../fdroid/data/InstalledAppProvider.java | 17 ++--------- .../fdroid/InstalledAppProviderTest.java | 18 +++++++++--- 4 files changed, 22 insertions(+), 46 deletions(-) diff --git a/src/org/fdroid/fdroid/PackageUpgradedReceiver.java b/src/org/fdroid/fdroid/PackageUpgradedReceiver.java index 516a9660d..404818881 100644 --- a/src/org/fdroid/fdroid/PackageUpgradedReceiver.java +++ b/src/org/fdroid/fdroid/PackageUpgradedReceiver.java @@ -39,11 +39,12 @@ public class PackageUpgradedReceiver extends PackageReceiver { Log.d("FDroid", "Updating installed app info for '" + appId + "' to v" + info.versionCode + " (" + info.versionName + ")"); - Uri uri = InstalledAppProvider.getAppUri(appId); + Uri uri = InstalledAppProvider.getContentUri(); ContentValues values = new ContentValues(1); + values.put(InstalledAppProvider.DataColumns.APP_ID, info.packageName); values.put(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode); values.put(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName); - context.getContentResolver().update(uri, values, null, null); + context.getContentResolver().insert(uri, values); } } \ No newline at end of file diff --git a/src/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java b/src/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java index 7d7b65d22..edb4ae5b3 100644 --- a/src/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java +++ b/src/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java @@ -27,7 +27,6 @@ public class InstalledAppCacheUpdater { private Context context; private List toInsert = new ArrayList(); - private List toUpdate = new ArrayList(); private List toDelete = new ArrayList(); protected InstalledAppCacheUpdater(Context context) { @@ -85,14 +84,13 @@ public class InstalledAppCacheUpdater { * then the cache has changed. */ private boolean hasChanged() { - return toInsert.size() > 0 || toUpdate.size() > 0 || toDelete.size() > 0; + return toInsert.size() > 0 || toDelete.size() > 0; } private void updateCache() { ArrayList ops = new ArrayList(); ops.addAll(deleteFromCache(toDelete)); - ops.addAll(updateCachedValues(toUpdate)); ops.addAll(insertIntoCache(toInsert)); if (ops.size() > 0) { @@ -114,12 +112,8 @@ public class InstalledAppCacheUpdater { List installedPackages = context.getPackageManager().getInstalledPackages(0); for (PackageInfo appInfo : installedPackages) { - if (!cachedInfo.containsKey(appInfo.packageName)) { - toInsert.add(appInfo); - } else { - if (cachedInfo.get(appInfo.packageName) < appInfo.versionCode) { - toUpdate.add(appInfo); - } + toInsert.add(appInfo); + if (cachedInfo.containsKey(appInfo.packageName)) { cachedInfo.remove(appInfo.packageName); } } @@ -148,22 +142,6 @@ public class InstalledAppCacheUpdater { return ops; } - private List updateCachedValues(List appsToUpdate) { - List ops = new ArrayList(appsToUpdate.size()); - if (appsToUpdate.size() > 0) { - Log.d(TAG, "Preparing to update installed app cache for " + appsToUpdate.size() + " apps."); - for (PackageInfo info : appsToUpdate) { - Uri uri = InstalledAppProvider.getAppUri(info.packageName); - ContentProviderOperation op = ContentProviderOperation.newUpdate(uri) - .withValue(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode) - .withValue(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName) - .build(); - ops.add(op); - } - } - return ops; - } - private List deleteFromCache(List appIds) { List ops = new ArrayList(appIds.size()); if (appIds.size() > 0) { diff --git a/src/org/fdroid/fdroid/data/InstalledAppProvider.java b/src/org/fdroid/fdroid/data/InstalledAppProvider.java index 20cacc656..249e4bf01 100644 --- a/src/org/fdroid/fdroid/data/InstalledAppProvider.java +++ b/src/org/fdroid/fdroid/data/InstalledAppProvider.java @@ -140,7 +140,7 @@ public class InstalledAppProvider extends FDroidProvider { } verifyVersionNameNotNull(values); - write().insertOrThrow(getTableName(), null, values); + write().replaceOrThrow(getTableName(), null, values); if (!isApplyingBatch()) { getContext().getContentResolver().notifyChange(uri, null); } @@ -149,20 +149,7 @@ public class InstalledAppProvider extends FDroidProvider { @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { - - if (matcher.match(uri) != CODE_SINGLE) { - throw new UnsupportedOperationException("Update not supported for " + uri + "."); - } - - QuerySelection query = new QuerySelection(where, whereArgs); - query = query.add(queryApp(uri.getLastPathSegment())); - - verifyVersionNameNotNull(values); - int count = write().update(getTableName(), values, query.getSelection(), query.getArgs()); - if (!isApplyingBatch()) { - getContext().getContentResolver().notifyChange(uri, null); - } - return count; + throw new UnsupportedOperationException("\"Update' not supported for installed appp provider. Instead, you should insert, and it will overwrite the relevant rows if one exists."); } /** diff --git a/test/src/org/fdroid/fdroid/InstalledAppProviderTest.java b/test/src/org/fdroid/fdroid/InstalledAppProviderTest.java index 017ac4e7a..14ff885c9 100644 --- a/test/src/org/fdroid/fdroid/InstalledAppProviderTest.java +++ b/test/src/org/fdroid/fdroid/InstalledAppProviderTest.java @@ -60,10 +60,20 @@ public class InstalledAppProviderTest extends FDroidProviderTest Date: Thu, 24 Apr 2014 21:26:43 +0100 Subject: [PATCH 279/282] Translation updates --- res/values-el/strings.xml | 1 + res/values-fr/strings.xml | 4 ++-- res/values-it/strings.xml | 4 ++-- res/values-nb/strings.xml | 4 ++-- res/values-pl/strings.xml | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index 80f806187..b161b91bf 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -59,6 +59,7 @@ Αναζήτηση Νέο Αποθετήριο Αφαίρεση Αποθετηρίου + Εύρεση τοπικών αποθετηρίων Εκτέλεση Διαμοιρασμός Εγκατάσταση diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 9a3003151..d0306be35 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -66,7 +66,7 @@ L\'URL d\'un dépôt ressemble à ceci : https://f-droid.org/repo Réception d\'application de Le NFC n\'est pas activé ! Allez dans les paramètres NFC… - Pas de méthode d\'envoi Bluetooth trouvée, choisissez en une ! + Pas de méthode d\'envoi Bluetooth trouvée, choisissez-en une ! Choisir la méthode d\'envoi Bluetooth Adresse du dépôt Empreinte digitale (optionnel) @@ -126,7 +126,7 @@ Voulez-vous les mettre à jour ? Tout Quoi de neuf ? Mis à jour récemment - Dépôt FDroid locaux + Dépôts FDroid locaux Découverte des dépôts FDroid locaux… Téléchargement %2$s / %3$s (%4$d%%) de diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 8b3835d67..4f4917204 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -9,7 +9,7 @@ Versione Modifica Elimina - Attiva Invio NFC… + Attiva Invio NFC... Cache applicazioni Conserva i file apk scaricati sulla scheda SD Non conservare i file apk @@ -127,7 +127,7 @@ Vuoi aggiornarlo? Novità Aggiornate di Recente Repo FDroid Locali - Rilevazione repo FDroid locali… + Rilevazione repo FDroid locali... Scaricamento %2$s / %3$s (%4$d%%) da %1$s diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index d6baa646c..a2ed8c4f6 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -75,7 +75,7 @@ Lisensiert GNU GPLv3. Listen over brukte register har endret seg. Vil du oppdatere dem? Oppdater registrene Endre registrene - Blåtann FDroid.apk… + Blåtann FDroid.apk... Innstillinger Om Søk @@ -122,7 +122,7 @@ Lisensiert GNU GPLv3. Det som er nytt Nylig oppdatert Lokale F-Droid register - Oppdager lokale FDroid register… + Oppdager lokale FDroid register... Laster ned %2$s / %3$s (%4$d%%) fra %1$s diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 6e709bf01..fc1cc4a16 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -150,7 +150,7 @@ Opis i inne szczegóły będą tu dostępne gdy zaktualizujesz repozytoriumUsunięcie repozytorium oznacza, że aplikacje z niego nie będą dłużej dostępne w F-Droid. Uwaga: Wszystkie poprzednio zainstalowane aplikacje zostaną na urządzeniu. - Zablokowane \"%1$s\". + Zablokowane \"%1%s\". Aby zainstalować aplikacje z tego repozytorium musisz je włączyć ponownie. %s lub później From 9bd33003a0397d20c96e9895679a2a430e73fb39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 25 Apr 2014 10:22:42 +0200 Subject: [PATCH 280/282] Add a script to fix format problems automatically --- res/values-pl/strings.xml | 2 +- tools/fix-formats.sh | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100755 tools/fix-formats.sh diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index fc1cc4a16..6e709bf01 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -150,7 +150,7 @@ Opis i inne szczegóły będą tu dostępne gdy zaktualizujesz repozytoriumUsunięcie repozytorium oznacza, że aplikacje z niego nie będą dłużej dostępne w F-Droid. Uwaga: Wszystkie poprzednio zainstalowane aplikacje zostaną na urządzeniu. - Zablokowane \"%1%s\". + Zablokowane \"%1$s\". Aby zainstalować aplikacje z tego repozytorium musisz je włączyć ponownie. %s lub później diff --git a/tools/fix-formats.sh b/tools/fix-formats.sh new file mode 100755 index 000000000..972ff6d1f --- /dev/null +++ b/tools/fix-formats.sh @@ -0,0 +1,5 @@ +#!/bin/bash -x + +# Fix StringFormatMatches programmatically + +sed -i 's/\(%[0-9]\)%\([a-z]\)/\1$\2/g' res/values*/*.xml From 3345a81077bd0a31bd1173da04010a98ca193927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 25 Apr 2014 10:23:23 +0200 Subject: [PATCH 281/282] Re-run fix-ellipsis --- res/values-it/strings.xml | 4 ++-- res/values-nb/strings.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 4f4917204..8b3835d67 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -9,7 +9,7 @@ Versione Modifica Elimina - Attiva Invio NFC... + Attiva Invio NFC… Cache applicazioni Conserva i file apk scaricati sulla scheda SD Non conservare i file apk @@ -127,7 +127,7 @@ Vuoi aggiornarlo? Novità Aggiornate di Recente Repo FDroid Locali - Rilevazione repo FDroid locali... + Rilevazione repo FDroid locali… Scaricamento %2$s / %3$s (%4$d%%) da %1$s diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index a2ed8c4f6..d6baa646c 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -75,7 +75,7 @@ Lisensiert GNU GPLv3. Listen over brukte register har endret seg. Vil du oppdatere dem? Oppdater registrene Endre registrene - Blåtann FDroid.apk... + Blåtann FDroid.apk… Innstillinger Om Søk @@ -122,7 +122,7 @@ Lisensiert GNU GPLv3. Det som er nytt Nylig oppdatert Lokale F-Droid register - Oppdager lokale FDroid register... + Oppdager lokale FDroid register… Laster ned %2$s / %3$s (%4$d%%) fra %1$s From d287dca85423931fb0c486879d831e06fafd8f3e Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 24 Apr 2014 16:48:50 +0930 Subject: [PATCH 282/282] Refactored SearchView into Activity + ListFragment (Fixes #11) This allowed for the use of LoaderCallbacks which seem like a better way at managing the lifecycle of the cursors which our ContentProviders return. --- src/org/fdroid/fdroid/SearchResults.java | 118 +++------------- .../fragments/SearchResultsFragment.java | 133 ++++++++++++++++++ 2 files changed, 156 insertions(+), 95 deletions(-) create mode 100644 src/org/fdroid/fdroid/views/fragments/SearchResultsFragment.java diff --git a/src/org/fdroid/fdroid/SearchResults.java b/src/org/fdroid/fdroid/SearchResults.java index 8f62100ba..53f475692 100644 --- a/src/org/fdroid/fdroid/SearchResults.java +++ b/src/org/fdroid/fdroid/SearchResults.java @@ -18,56 +18,22 @@ package org.fdroid.fdroid; -import android.app.ListActivity; -import android.app.SearchManager; import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; import android.os.Bundle; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.ListView; -import android.widget.TextView; - +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; import android.support.v4.app.NavUtils; import android.support.v4.view.MenuItemCompat; - +import android.view.Menu; +import android.view.MenuItem; +import android.widget.LinearLayout; import org.fdroid.fdroid.compat.ActionBarCompat; -import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.AppProvider; -import org.fdroid.fdroid.views.AppListAdapter; -import org.fdroid.fdroid.views.AvailableAppListAdapter; -import org.fdroid.fdroid.views.fragments.AppListFragment; +import org.fdroid.fdroid.views.fragments.SearchResultsFragment; -public class SearchResults extends ListActivity { - - private static final int REQUEST_APPDETAILS = 0; +public class SearchResults extends FragmentActivity { private static final int SEARCH = Menu.FIRST; - private Cursor cursor; - private AppListAdapter adapter; - - protected String getQuery() { - Intent intent = getIntent(); - String query; - if (Intent.ACTION_SEARCH.equals(intent.getAction())) { - query = intent.getStringExtra(SearchManager.QUERY); - } else { - Uri data = intent.getData(); - if (data != null && data.isHierarchical()) { - query = data.getQueryParameter("q"); - if (query != null && query.startsWith("pname:")) - query = query.substring(6); - } else { - query = data.getEncodedSchemeSpecificPart(); - } - } - return query; - } - @Override public void onCreate(Bundle savedInstanceState) { @@ -75,21 +41,28 @@ public class SearchResults extends ListActivity { super.onCreate(savedInstanceState); - setContentView(R.layout.searchresults); + // Start a search by just typing + setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); + + FragmentManager fm = getSupportFragmentManager(); + if (fm.findFragmentById(android.R.id.content) == null) { + + // Need to set a dummy view (which will get overridden by the fragment manager + // below) so that we can call setContentView(). This is a work around for + // a (bug?) thing in 3.0, 3.1 which requires setContentView to be invoked before + // the actionbar is played with: + // http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html + setContentView( new LinearLayout(this) ); + + SearchResultsFragment fragment = new SearchResultsFragment(); + fm.beginTransaction().add(android.R.id.content, fragment).commit(); + } // Actionbar cannot be accessed until after setContentView (on 3.0 and 3.1 devices) // see: http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html // for reason why. ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); - // Start a search by just typing - setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); - } - - @Override - protected void onResume() { - super.onResume(); - updateView(); } @Override @@ -98,51 +71,6 @@ public class SearchResults extends ListActivity { setIntent(intent); } - private void updateView() { - - String query = getQuery(); - - if (query != null) - query = query.trim(); - - if (query == null || query.length() == 0) - finish(); - - if (cursor != null) cursor.close(); - cursor = managedQuery( - AppProvider.getSearchUri(query), AppListFragment.APP_PROJECTION, - null, null, AppListFragment.APP_SORT); - - - TextView tv = (TextView) findViewById(R.id.description); - String headertext; - int count = cursor != null ? cursor.getCount() : 0; - if (count == 0) { - headertext = getString(R.string.searchres_noapps, query); - } else if (count == 1) { - headertext = getString(R.string.searchres_oneapp, query); - } else { - headertext = getString(R.string.searchres_napps, count, query); - } - tv.setText(headertext); - Log.d("FDroid", "Search for '" + query + "' returned " + count + " results"); - - adapter = new AvailableAppListAdapter(this, cursor); - getListView().setFastScrollEnabled(true); - setListAdapter(adapter); - } - - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - final App app; - app = new App((Cursor) adapter.getItem(position)); - - Intent intent = new Intent(this, AppDetails.class); - intent.putExtra(AppDetails.EXTRA_APPID, app.id); - startActivityForResult(intent, REQUEST_APPDETAILS); - super.onListItemClick(l, v, position, id); - } - @Override public boolean onCreateOptionsMenu(Menu menu) { diff --git a/src/org/fdroid/fdroid/views/fragments/SearchResultsFragment.java b/src/org/fdroid/fdroid/views/fragments/SearchResultsFragment.java new file mode 100644 index 000000000..a2f017be8 --- /dev/null +++ b/src/org/fdroid/fdroid/views/fragments/SearchResultsFragment.java @@ -0,0 +1,133 @@ +package org.fdroid.fdroid.views.fragments; + +import android.app.SearchManager; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; +import android.widget.TextView; +import org.fdroid.fdroid.AppDetails; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.views.AppListAdapter; +import org.fdroid.fdroid.views.AvailableAppListAdapter; + +public class SearchResultsFragment extends ListFragment implements LoaderManager.LoaderCallbacks { + + private static final String TAG = "org.fdroid.fdroid.views.fragments.SearchResultsFragment"; + + private static final int REQUEST_APPDETAILS = 0; + + private AppListAdapter adapter; + + protected String getQuery() { + Intent intent = getActivity().getIntent(); + String query = null; + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + query = intent.getStringExtra(SearchManager.QUERY); + } else { + Uri data = intent.getData(); + if (data != null && data.isHierarchical()) { + query = data.getQueryParameter("q"); + if (query != null && query.startsWith("pname:")) + query = query.substring(6); + } else if (data!= null ) { + query = data.getEncodedSchemeSpecificPart(); + } + } + return query == null ? "" : query; + } + + @Override + public void onResume() { + super.onResume(); + + //Starts a new or restarts an existing Loader in this manager + getLoaderManager().restartLoader(0, null, this); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup root, Bundle data) { + + adapter = new AvailableAppListAdapter(getActivity(), null); + setListAdapter(adapter); + + View view = inflater.inflate(R.layout.searchresults, null); + updateSummary(view); + + return view; + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + Uri uri = AppProvider.getSearchUri(getQuery()); + return new CursorLoader( + getActivity(), + uri, + AppListFragment.APP_PROJECTION, + null, + null, + AppListFragment.APP_SORT + ); + } + + private void updateSummary() { + updateSummary(getView()); + } + + private void updateSummary(View view) { + + String query = getQuery(); + + if (query != null) + query = query.trim(); + + if (query == null || query.length() == 0) + getActivity().finish(); + + TextView tv = (TextView) view.findViewById(R.id.description); + String headerText; + int count = adapter.getCount(); + if (count == 0) { + headerText = getString(R.string.searchres_noapps, query); + } else if (count == 1) { + headerText = getString(R.string.searchres_oneapp, query); + } else { + headerText = getString(R.string.searchres_napps, count, query); + } + tv.setText(headerText); + Log.d(TAG, "Search for '" + query + "' returned " + count + " results"); + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + final App app; + app = new App((Cursor) adapter.getItem(position)); + + Intent intent = new Intent(getActivity(), AppDetails.class); + intent.putExtra(AppDetails.EXTRA_APPID, app.id); + startActivityForResult(intent, REQUEST_APPDETAILS); + super.onListItemClick(l, v, position, id); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + adapter.swapCursor(data); + updateSummary(); + } + + @Override + public void onLoaderReset(Loader loader) { + adapter.swapCursor(null); + } +}
  • ^U9PAU*=wh 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�-787IIN`jRP54mH7?GUV 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