Compare commits

...

18 Commits

Author SHA1 Message Date
Hans-Christoph Steiner
0ba6299990 Bump to 0.102.3 2017-04-05 21:48:08 +02:00
Chirayu Desai
bc6c4fbc80 InstallerFactory: Check for privext before checking if apk to be installed is privext
* Move the privileged extension installed check above
  the check whether the apk to be installed is privext.
* This lets privext updates work when it is already installed.
2017-04-01 17:20:43 +02:00
Chirayu Desai
377b57f985 Set installer package name to privext if using that
* The new PackageInstaller APIs, being used by the privext on Android
  7.0 and above aren't happy with uninstall being done by an app
  other than the original installer.
* Set it to the privileged extension if that is enabled and being used,
  to make uninstalling work

closes !457
2017-04-01 16:22:50 +02:00
Peter Serwylo
aab60aec4e Ignore errors that are likely due to filesystem corruption.
There is a specific POSIX error "EIO" which seems to be the "general
purpose we don't know what went wrong but its probably bad" exception.
Our investigations in #855 resulted in the conclusion that it is likely
due to some sort of filesystem corruption or something like that.

Either way, it is annoying many people, so we need to prevent it or
ignore it, rather than prompting the user to submit a bug report.
After much investigation it was unable to be reproduced other than by
one of the original bug reporters. As such, this change ignores it.

Unfortunately Java `IOException`s don't have an API for getting the
errno of a POSIX IO error. Thus, this change results to parsing the
exception message instead :(

Fixes #855.
Refs !440
2017-04-01 16:19:53 +02:00
Hans-Christoph Steiner
4eb37b0d33 pull in .gitlab-ci.yml from master to get connected24 working again 2017-04-01 16:19:53 +02:00
Chirayu Desai
6b26eb661f gitlab-ci: Use ARM emulator for API 24 as well
* x86 emulator requires kvm which isn't always available on the
  GitLab CI runners.
2017-04-01 16:19:53 +02:00
Chirayu Desai
46fc6dbbfd InstallManagerService: Clear notification on privileged install complete
* Without privileged extension, the install proceeds to user interaction
  which is where the notification is cleared.
* When the privileged extension is installed that doesn't happen,
  so the download notification doesn't get cleared
2017-03-25 15:42:21 +05:30
Hans-Christoph Steiner
4f420c55d0 Bump to 0.102.2 2017-03-14 21:57:45 +01:00
Chirayu Desai
9ded5eb974 ApkFileProvider: Explicitly grant read permission to PrivExt 2017-02-28 19:46:04 +01:00
Chirayu Desai
2a7a8ac38b PrivilegedInstaller: Use ContentUri on Nougat (24) and above 2017-02-28 19:46:01 +01:00
Hans-Christoph Steiner
f5065c93a8 always refresh APKs in DB at start with timestamps < 2010-01-01
APKs installed in /system} will often have zeroed out timestamps, like
2008-01-01 (ziptime) or 2009-01-01.  So instead anything older than 2010
every time since we have no way to know whether an APK wasn't changed as
part of an OTA update.  An OTA update could change the APK without changing
the versionCode or lastUpdateTime.

closes #819
2017-02-28 19:43:55 +01:00
Hans-Christoph Steiner
7b802eaf2f Bump to 0.102.1 2017-02-24 17:14:37 +01:00
Hans-Christoph Steiner
4090132d84 Merge branch 'investigate-854--uninstall-error' into stable-v0.102.1
Ensure current app state is reflected in AppDetails when resuming.

See merge request !429
2017-02-24 15:37:48 +01:00
Peter Serwylo
242cdb5dd4 Make 3rd party AppCompatListPreference adhere to our checkstyle rules.
Added braces around one line if statements.
2017-02-24 15:32:56 +01:00
Peter Serwylo
1fdb573774 Use AppCompatListPreference from QuickLyric to fix dialog themes.
Fixes Issue #750.

This new class makes sure to use the correct `AlertDialog.Builder` from
the support lib. This in turn ensures the correct styles get applied to
the result alert dialog.
2017-02-24 15:32:53 +01:00
Peter Serwylo
88ee9849f7 Use AppCompatActivity instead of deprecated ActionBarActivity
Doesn't change anything, just removes a deprecation warning.
AppCompatActivity currently extends ActionBarActivity and doesn't
provide any further imnplementation.
2017-02-24 15:32:48 +01:00
Peter Serwylo
94403d1d7e When resuming the activity check for uninstall/install in the mean time.
Fixes #854.

There is a bunch of UI code which runs in `onResumeFragments()`. However
it all depends on the `app` variable in `AppDetails` being up to date.
There is a number of things which could've changed since last refresh,
the most notable of which is that the app may have been
installed/uninstalled. Thus, this particular change checks only for a
difference in installed state. However it could equally do a deeper
comparison between the old and new `App` objects if it becomes aparant
that other state becomes stale between pause and resume in the future.

It is true that this requires a database query to be run each time the
activity is resumed, but it doesn't seem there is much else we can do
to prevent bugs relating to stale state.
2017-02-15 13:45:07 +11:00
Peter Serwylo
ff6a60ae26 Make exception more explicit by including root cause and package name.
By omitting the cause of the exception, we are getting sub-standard
stack traces. I suspect where the root cause is, because it is only
thrown when a `PackageManager.NameNotFoundException` is thrown, but it
would help if that was in the ACRA reports to stop future investigators
from having to track that down.

Also add the name of the package which we searched for in the exception.
Knowing which apk was being uninstalled is not particularly helpful for
debugging, but knowing if it is `null` or `""` is important, because
that means that the `app.packageName` variable is not populated
correctly.
2017-02-15 10:33:36 +11:00
13 changed files with 214 additions and 48 deletions

View File

@ -1,4 +1,4 @@
image: fdroid/ci:client-20161023
image: registry.gitlab.com/fdroid/ci-images:client
cache:
paths:
@ -7,13 +7,21 @@ cache:
before_script:
- export GRADLE_USER_HOME=$PWD/.gradle
- export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' app/build.gradle`
- echo y | android --silent update sdk --no-ui --filter android-${ANDROID_COMPILE_SDK}
test:
script:
- cd app
- ./tools/langs-list-check.py
- ./tools/check-string-format.py
- cd ..
- ./gradlew assemble -PdisablePreDex
# always report on lint errors to the build log
- sed -i -e 's,textReport .*,textReport true,' app/build.gradle
- ./gradlew lint -PdisablePreDex
- ./gradlew pmd -PdisablePreDex
- ./gradlew checkstyle -PdisablePreDex
- ./gradlew test -PdisablePreDex || {
for log in app/build/reports/*ests/*/*ml; do
echo "read $log here:";
@ -26,6 +34,7 @@ connected10:
variables:
AVD_SDK: "10"
script:
- ./gradlew assembleDebug -PdisablePreDex
- emulator64-arm -avd fcl-test-$AVD_SDK -no-skin -no-audio -no-window &
- ./tools/wait-for-emulator
- adb shell input keyevent 82 &
@ -47,7 +56,8 @@ connected24:
variables:
AVD_SDK: "24"
script:
- emulator64-x86 -avd fcl-test-$AVD_SDK -no-skin -no-audio -no-window &
- ./gradlew assembleDebug -PdisablePreDex
- emulator64-arm -avd fcl-test-$AVD_SDK -no-audio -no-window &
- ./tools/wait-for-emulator
- adb shell input keyevent 82 &
- export EXITVALUE=0
@ -64,24 +74,6 @@ connected24:
done
- exit $EXITVALUE
pmd:
script:
- ./gradlew pmd -PdisablePreDex
checkstyle:
script:
- ./gradlew checkstyle -PdisablePreDex
tools:
before_script:
- echo "ignored, no gradle needed"
script:
- cd app
- ./tools/langs-list-check.py
- ./tools/check-string-format.py
after_script:
- echo "ignored, no gradle needed"
after_script:
# this file changes every time but should not be cached
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock

View File

@ -170,7 +170,7 @@ android {
}
defaultConfig {
versionCode 102050
versionCode 102350
versionName getVersionName()
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

View File

@ -0,0 +1,100 @@
/*
* *
* * This file is part of QuickLyric
* * Created by geecko
* *
* * QuickLyric 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.
* *
* * QuickLyric 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 QuickLyric. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.geecko.QuickLyric.view;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.PreferenceManager;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatDialog;
import android.util.AttributeSet;
import java.lang.reflect.Method;
public class AppCompatListPreference extends ListPreference {
private AppCompatDialog mDialog;
public AppCompatListPreference(Context context) {
super(context);
}
public AppCompatListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public AppCompatDialog getDialog() {
return mDialog;
}
@Override
protected void showDialog(Bundle state) {
if (getEntries() == null || getEntryValues() == null) {
throw new IllegalStateException(
"ListPreference requires an entries array and an entryValues array.");
}
int preselect = findIndexOfValue(getValue());
AlertDialog.Builder builder = new AlertDialog.Builder(getContext())
.setTitle(getDialogTitle())
.setIcon(getDialogIcon())
.setSingleChoiceItems(getEntries(), preselect, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (which >= 0 && getEntryValues() != null) {
String value = getEntryValues()[which].toString();
if (callChangeListener(value) && isPersistent()) {
setValue(value);
}
}
dialog.dismiss();
}
});
PreferenceManager pm = getPreferenceManager();
try {
Method method = pm.getClass().getDeclaredMethod(
"registerOnActivityDestroyListener",
PreferenceManager.OnActivityDestroyListener.class);
method.setAccessible(true);
method.invoke(pm, this);
} catch (Exception e) {
e.printStackTrace();
}
mDialog = builder.create();
if (state != null) {
mDialog.onRestoreInstanceState(state);
}
mDialog.show();
}
@Override
public void onActivityDestroy() {
super.onActivityDestroy();
if (mDialog != null && mDialog.isShowing() &&
mDialog.getWindow() != null && mDialog.getWindow().getWindowManager() != null) {
mDialog.dismiss();
}
}
}

View File

@ -432,6 +432,15 @@ public class AppDetails extends AppCompatActivity {
myAppObserver);
}
@Override
protected void onResume() {
App newApp = AppProvider.Helper.findHighestPriorityMetadata(getContentResolver(), app.packageName);
if (newApp.isInstalled() != app.isInstalled()) {
setApp(newApp);
}
super.onResume();
}
@Override
protected void onResumeFragments() {
// Must be called before super.onResumeFragments(), as the fragments depend on the active
@ -993,7 +1002,7 @@ public class AppDetails extends AppCompatActivity {
return apk;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
throw new IllegalStateException("Couldn't find app while installing");
throw new IllegalStateException("Couldn't find installed apk for " + app.packageName, e);
}
}

View File

@ -22,13 +22,13 @@ import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.NavUtils;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.app.AppCompatActivity;
import android.view.MenuItem;
import android.widget.LinearLayout;
import org.fdroid.fdroid.views.fragments.PreferencesFragment;
public class PreferencesActivity extends ActionBarActivity {
public class PreferencesActivity extends AppCompatActivity {
public static final int RESULT_RESTART = 4;

View File

@ -408,13 +408,29 @@ public final class Utils {
byte[] mdbytes = md.digest();
return toHexString(mdbytes).toLowerCase(Locale.ENGLISH);
} catch (IOException | NoSuchAlgorithmException e) {
} catch (IOException e) {
// The annoyance (potentially) caused by miscellaneous filesystem corruption results in
// F-Droid constantly popping up crash reports when F-Droid isn't even open. As such this
// exception-message-parsing-and-throwing-a-new-ignorable-exception-hackery is probably
// warranted. See https://www.gitlab.com/fdroid/fdroidclient/issues/855 for more detail.
if (e.getMessage().contains("read failed: EIO (I/O error)")) {
throw new PotentialFilesystemCorruptionException(e);
}
throw new IllegalArgumentException(e);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
} finally {
closeQuietly(fis);
}
}
public static class PotentialFilesystemCorruptionException extends IllegalArgumentException {
public PotentialFilesystemCorruptionException(IOException e) {
super(e);
}
}
/**
* Computes the base 16 representation of the byte array argument.
*

View File

@ -1,10 +1,12 @@
package org.fdroid.fdroid.compat;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import org.fdroid.fdroid.installer.PrivilegedInstaller;
import org.fdroid.fdroid.Utils;
public class PackageManagerCompat {
@ -12,10 +14,20 @@ public class PackageManagerCompat {
private static final String TAG = "PackageManagerCompat";
@TargetApi(11)
public static void setInstaller(PackageManager mPm, String packageName) {
public static void setInstaller(Context context, PackageManager mPm, String packageName) {
if (Build.VERSION.SDK_INT < 11) return;
try {
/*
* Starting with 7.0 (API 24), we're using PackageInstaller APIs
* to install and uninstall apps via the privileged extension.
* That enforces the uninstaller being the same as the installer,
* so set the package name to that.
*/
if (Build.VERSION.SDK_INT >= 24 && PrivilegedInstaller.isDefault(context)) {
mPm.setInstallerPackageName(packageName, "org.fdroid.fdroid.privileged");
} else {
mPm.setInstallerPackageName(packageName, "org.fdroid.fdroid");
}
Utils.debugLog(TAG, "Installer package name for " + packageName + " set successfully");
} catch (Exception e) {
// Many problems can occur:

View File

@ -8,13 +8,19 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.support.annotation.Nullable;
import android.util.Log;
import android.widget.Toast;
import org.acra.ACRA;
import org.fdroid.fdroid.Hasher;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.InstalledAppTable;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
import rx.subjects.PublishSubject;
import java.io.File;
import java.io.FilenameFilter;
@ -23,10 +29,6 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
import rx.subjects.PublishSubject;
/**
* Handles all updates to {@link InstalledAppProvider}, whether checking the contents
* versus what Android says is installed, or processing {@link Intent}s that come
@ -124,13 +126,21 @@ public class InstalledAppProviderService extends IntentService {
* is in sync with what the {@link PackageManager} tells us is installed. Once
* completed, the relevant {@link android.content.ContentProvider}s will be
* notified of any changes to installed statuses.
* <p/>
* <p>
* The installed app cache could get out of sync, e.g. if F-Droid crashed/ or
* ran out of battery half way through responding to {@link Intent#ACTION_PACKAGE_ADDED}.
* This method returns immediately, and will continue to work in an
* {@link IntentService}. It doesn't really matter where we put this in the
* bootstrap process, because it runs in its own thread, at the lowest priority:
* {@link Process#THREAD_PRIORITY_LOWEST}.
* <p>
* APKs installed in {@code /system} will often have zeroed out timestamps, like
* 2008-01-01 (ziptime) or 2009-01-01. So instead anything older than 2010 every
* time since we have no way to know whether an APK wasn't changed as part of an
* OTA update. An OTA update could change the APK without changing the
* {@link PackageInfo#versionCode} or {@link PackageInfo#lastUpdateTime}.
*
* @see <a href="https://gitlab.com/fdroid/fdroidclient/issues/819>issue #819</a>
*/
public static void compareToPackageManager(Context context) {
Map<String, Long> cachedInfo = InstalledAppProvider.Helper.all(context);
@ -139,7 +149,8 @@ public class InstalledAppProviderService extends IntentService {
.getInstalledPackages(PackageManager.GET_SIGNATURES);
for (PackageInfo packageInfo : packageInfoList) {
if (cachedInfo.containsKey(packageInfo.packageName)) {
if (packageInfo.lastUpdateTime > cachedInfo.get(packageInfo.packageName)) {
if (packageInfo.lastUpdateTime < 1262300400000L // 2010-01-01 00:00
|| packageInfo.lastUpdateTime > cachedInfo.get(packageInfo.packageName)) {
insert(context, packageInfo);
}
cachedInfo.remove(packageInfo.packageName);
@ -188,6 +199,22 @@ public class InstalledAppProviderService extends IntentService {
String hashType = "sha256";
String hash = Utils.getBinaryHash(apk, hashType);
insertAppIntoDb(this, packageInfo, hashType, hash);
} catch (final Utils.PotentialFilesystemCorruptionException e) {
Log.e(TAG, "Encountered potential filesystem corruption, or other unknown " +
"problem when calculating hash of " + apk.getAbsolutePath() + ". " +
"It is unlikely F-Droid can do anything about this, and this " +
"likely happened in the background. As such, we will continue without " +
"interrupting the user by asking them to send a crash report.");
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
Context context = getApplicationContext();
Toast.makeText(context, e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
});
return;
} catch (IllegalArgumentException e) {
Utils.debugLog(TAG, e.getMessage());
ACRA.getErrorReporter().handleException(e, false);
@ -207,7 +234,7 @@ public class InstalledAppProviderService extends IntentService {
* broadcast. In the first case, it will already have a {@link PackageInfo} for us. However if
* it is from the later case, we'll need to query the {@link PackageManager} ourselves to get
* this info.
*
* <p>
* Can still return null, as there is potentially race conditions to do with uninstalling apps
* such that querying the {@link PackageManager} for a given package may throw an exception.
*/

View File

@ -20,6 +20,7 @@
package org.fdroid.fdroid.installer;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.v4.content.FileProvider;
@ -59,8 +60,9 @@ public class ApkFileProvider extends FileProvider {
if (useContentUri) {
// return a content Uri using support libs FileProvider
return getUriForFile(context, AUTHORITY, sanitizedApkFile);
Uri apkUri = getUriForFile(context, AUTHORITY, sanitizedApkFile);
context.grantUriPermission("org.fdroid.fdroid.privileged", apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
return apkUri;
}
// Need the apk to be world readable, so that the installer is able to read it.

View File

@ -334,7 +334,10 @@ public class InstallManagerService extends Service {
case Installer.ACTION_INSTALL_COMPLETE:
Apk apkComplete = removeFromActive(downloadUrl);
PackageManagerCompat.setInstaller(getPackageManager(), apkComplete.packageName);
PackageManagerCompat.setInstaller(context, getPackageManager(), apkComplete.packageName);
if (PrivilegedInstaller.isDefault(context)) {
cancelNotification(downloadUrl);
}
localBroadcastManager.unregisterReceiver(this);
break;

View File

@ -45,12 +45,12 @@ public class InstallerFactory {
}
Installer installer;
if (apk.packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) {
// special case for installing "Privileged Extension" with root
installer = new ExtensionInstaller(context, apk);
} else if (PrivilegedInstaller.isDefault(context)) {
if (PrivilegedInstaller.isDefault(context)) {
Utils.debugLog(TAG, "privileged extension correctly installed -> PrivilegedInstaller");
installer = new PrivilegedInstaller(context, apk);
} else if (apk.packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) {
// special case for installing "Privileged Extension" with root
installer = new ExtensionInstaller(context, apk);
} else {
installer = new DefaultInstaller(context, apk);
}

View File

@ -27,12 +27,14 @@ import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.compat.PackageManagerCompat;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.privileged.IPrivilegedCallback;
import org.fdroid.fdroid.privileged.IPrivilegedService;
@ -393,6 +395,10 @@ public class PrivilegedInstaller extends Installer {
}
};
/*
* Set installer to the privileged extension
*/
PackageManagerCompat.setInstaller(context, context.getPackageManager(), apk.packageName);
Intent serviceIntent = new Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT);
serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME);
context.getApplicationContext().bindService(serviceIntent, mServiceConnection,
@ -406,7 +412,6 @@ public class PrivilegedInstaller extends Installer {
@Override
protected boolean supportsContentUri() {
// TODO: correct?
return false;
return Build.VERSION.SDK_INT >= 24;
}
}

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/updates">
<ListPreference android:title="@string/update_interval"
<com.geecko.QuickLyric.view.AppCompatListPreference android:title="@string/update_interval"
android:key="updateInterval"
android:defaultValue="24"
android:entries="@array/updateIntervalNames"
@ -24,12 +24,12 @@
android:title="@string/update_history" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/display">
<ListPreference android:title="@string/pref_language"
<com.geecko.QuickLyric.view.AppCompatListPreference android:title="@string/pref_language"
android:key="language"
android:defaultValue=""
android:entries="@array/languageNames"
android:entryValues="@array/languageValues" />
<ListPreference android:title="@string/theme"
<com.geecko.QuickLyric.view.AppCompatListPreference android:title="@string/theme"
android:key="theme"
android:defaultValue="light"
android:entries="@array/themeNames"
@ -77,7 +77,7 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/other"
android:key="pref_category_other">
<ListPreference android:title="@string/cache_downloaded"
<com.geecko.QuickLyric.view.AppCompatListPreference android:title="@string/cache_downloaded"
android:key="keepCacheFor"
android:defaultValue="86400000"
android:entries="@array/keepCacheNames"