Merge branch 'swap-and-installer-improvements' into 'master'

Swap and installer improvements

See merge request fdroid/fdroidclient!733
This commit is contained in:
Hans-Christoph Steiner 2018-08-17 14:05:20 +00:00
commit 795dd0dbf7
16 changed files with 172 additions and 32 deletions

View File

@ -161,15 +161,15 @@ dependencies {
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.8.7'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.8.7'
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
fullImplementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
implementation 'org.bouncycastle:bcprov-jdk15on:1.60'
fullImplementation 'org.bouncycastle:bcpkix-jdk15on:1.60'
fullImplementation 'cc.mvdan.accesspoint:library:0.2.0'
fullImplementation 'org.jmdns:jmdns:3.5.3'
fullImplementation 'org.nanohttpd:nanohttpd:2.3.1'
testImplementation 'org.robolectric:robolectric:3.8'
testImplementation "com.android.support.test:monitor:1.0.2"
testImplementation 'org.bouncycastle:bcprov-jdk15on:1.59'
testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.7.22'

View File

@ -147,10 +147,24 @@ public final class LocalRepoManager {
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(indexHtml)));
StringBuilder builder = new StringBuilder();
for (App app : apps.values()) {
builder.append("<li><a href=\"/fdroid/repo/")
.append(app.installedApk.apkName)
.append("\"><img width=\"32\" height=\"32\" src=\"/fdroid/repo/icons/")
.append(app.packageName)
.append("_")
.append(app.installedApk.versionCode)
.append(".png\">")
.append(app.name)
.append("</a></li>\n");
}
String line;
while ((line = in.readLine()) != null) {
line = line.replaceAll("\\{\\{REPO_URL\\}\\}", repoAddress);
line = line.replaceAll("\\{\\{CLIENT_URL\\}\\}", fdroidClientURL);
line = line.replaceAll("\\{\\{APP_LIST\\}\\}", builder.toString());
out.write(line);
}
in.close();

View File

@ -257,7 +257,9 @@ public class StartSwapView extends RelativeLayout implements SwapWorkflowActivit
textBluetoothVisible.setText(textResource);
bluetoothSwitch = (SwitchCompat) findViewById(R.id.switch_bluetooth);
Utils.debugLog(TAG, getManager().isBluetoothDiscoverable() ? "Initially marking switch as checked, because Bluetooth is discoverable." : "Initially marking switch as not-checked, because Bluetooth is not discoverable.");
Utils.debugLog(TAG, getManager().isBluetoothDiscoverable()
? "Initially marking switch as checked, because Bluetooth is discoverable."
: "Initially marking switch as not-checked, because Bluetooth is not discoverable.");
bluetoothSwitch.setOnCheckedChangeListener(onBluetoothSwitchToggled);
setBluetoothSwitchState(getManager().isBluetoothDiscoverable(), true);

View File

@ -129,7 +129,7 @@
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/install_history_file_provider"/>
android:resource="@xml/installer_file_provider"/>
</provider>
<activity

View File

@ -71,6 +71,28 @@
color: #fff;
}
details {
margin-left: 1em;
}
ul {
list-style-type: none;
}
ul > li {
padding: 1em 0;
}
ul > li a {
font-size: xx-large;
text-decoration: none;
color: #fff;
}
ul > li a img {
padding-right: 0.5em;
}
#download-from-web {
padding-left: 2em;
padding-right: 2em;
@ -105,5 +127,12 @@
<img src="swap-tick-not-done.png" class="tick not-done" alt="Not done" />
</li>
</ol>
<br/><br/><br/><br/>
<details>
<summary>Available Apps</summary>
<ul>
{{APP_LIST}}
</ul>
</details>
</body>
</html>

View File

@ -580,7 +580,6 @@ public class FDroidApp extends Application {
String bluetoothPackageName = null;
String className = null;
boolean found = false;
Intent sendBt = null;
try {
@ -599,20 +598,19 @@ public class FDroidApp extends Application {
if ("com.android.bluetooth".equals(bluetoothPackageName)
|| "com.mediatek.bluetooth".equals(bluetoothPackageName)) {
className = info.activityInfo.name;
found = true;
break;
}
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Could not get application info to send via bluetooth", e);
found = false;
className = null;
} catch (IOException e) {
Exception toLog = new RuntimeException("Error preparing file to send via Bluetooth", e);
ACRA.getErrorReporter().handleException(toLog, false);
}
if (sendBt != null) {
if (found) {
if (className != null) {
sendBt.setClassName(bluetoothPackageName, className);
activity.startActivity(sendBt);
} else {

View File

@ -37,14 +37,18 @@ import java.io.IOException;
* either locally or for sending via bluetooth.
* <p/>
* APK handling for installations:
* 1. APKs are downloaded into a cache directory that is either created on SD card
* <ol>
* <li>APKs are downloaded into a cache directory that is either created on SD card
* <i>"/Android/data/[app_package_name]/cache/apks"</i> (if card is mounted and app has
* appropriate permission) or on device's file system depending incoming parameters.
* 2. Before installation, the APK is copied into the private data directory of the F-Droid,
* <i>"/data/data/[app_package_name]/files/install-$random.apk"</i>.
* 3. The hash of the file is checked against the expected hash from the repository
* 4. For Android < 7, a file Uri pointing to the File is returned, for Android >= 7,
* a content Uri is returned using support lib's FileProvider.
* appropriate permission) or on device's file system depending incoming parameters</li>
* <li>Before installation, the APK is copied into the private data directory of the F-Droid,
* <i>"/data/data/[app_package_name]/files/install-$random.apk"</i></li>
* <li>The hash of the file is checked against the expected hash from the repository</li>
* <li>For {@link Build.VERSION_CODES#M < android-23}, a {@code file://} {@link Uri}
* pointing to the {@link File} is returned, for {@link Build.VERSION_CODES#M >= android-23},
* a {@code content://} {@code Uri} is returned using support lib's
* {@link FileProvider}</li>
* </ol>
*/
public class ApkFileProvider extends FileProvider {
@ -52,7 +56,7 @@ public class ApkFileProvider extends FileProvider {
public static Uri getSafeUri(Context context, PackageInfo packageInfo) throws IOException {
SanitizedFile tempApkFile = ApkCache.copyInstalledApkToFiles(context, packageInfo);
return getSafeUri(context, tempApkFile, Build.VERSION.SDK_INT >= 24);
return getSafeUri(context, tempApkFile, Build.VERSION.SDK_INT >= 23);
}
/**
@ -89,12 +93,12 @@ public class ApkFileProvider extends FileProvider {
context.grantUriPermission(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME,
apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
context.grantUriPermission("com.android.bluetooth", apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
context.grantUriPermission("com.mediatek.bluetooth", apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
return apkUri;
} else {
tempFile.setReadable(true, false);
return Uri.fromFile(tempFile);
}
tempFile.setReadable(true, false);
return Uri.fromFile(tempFile);
}
}

View File

@ -28,7 +28,6 @@ import android.net.Uri;
import android.os.Process;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
@ -47,8 +46,7 @@ import java.util.List;
public class InstallHistoryService extends IntentService {
public static final String TAG = "InstallHistoryService";
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".installer";
public static final Uri LOG_URI = Uri.parse("content://" + AUTHORITY + "/install_history/all");
public static final Uri LOG_URI = Uri.parse("content://" + Installer.AUTHORITY + "/install_history/all");
private static BroadcastReceiver broadcastReceiver;

View File

@ -31,6 +31,7 @@ import android.os.PatternMatcher;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
@ -51,6 +52,8 @@ public abstract class Installer {
final Context context;
final Apk apk;
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".installer";
public static final String ACTION_INSTALL_STARTED = "org.fdroid.fdroid.installer.Installer.action.INSTALL_STARTED";
public static final String ACTION_INSTALL_COMPLETE = "org.fdroid.fdroid.installer.Installer.action.INSTALL_COMPLETE";
public static final String ACTION_INSTALL_INTERRUPTED = "org.fdroid.fdroid.installer.Installer.action.INSTALL_INTERRUPTED";

View File

@ -7,10 +7,12 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.DrawableRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.TextViewCompat;
import android.support.v7.app.AlertDialog;
@ -28,6 +30,7 @@ import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.MimeTypeMap;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
@ -35,6 +38,7 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.apache.commons.io.FilenameUtils;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
@ -43,10 +47,12 @@ import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.privileged.views.AppDiff;
import org.fdroid.fdroid.privileged.views.AppSecurityPermissions;
import org.fdroid.fdroid.views.main.MainActivity;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@ -512,9 +518,42 @@ public class AppDetailsRecyclerViewAdapter
buttonPrimaryView.setText(R.string.menu_upgrade);
buttonPrimaryView.setOnClickListener(onUpgradeClickListener);
} else {
Apk mediaApk = app.getMediaApkifInstalled(context);
if (context.getPackageManager().getLaunchIntentForPackage(app.packageName) != null) {
buttonPrimaryView.setText(R.string.menu_launch);
buttonPrimaryView.setOnClickListener(onLaunchClickListener);
} else if (!app.isApk && mediaApk != null) {
final File installedFile = new File(mediaApk.getMediaInstallPath(context), mediaApk.apkName);
if (!installedFile.toString().startsWith(context.getApplicationInfo().dataDir)) {
final Intent viewIntent = new Intent(Intent.ACTION_VIEW);
Uri uri = FileProvider.getUriForFile(context, Installer.AUTHORITY, installedFile);
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
FilenameUtils.getExtension(installedFile.getName()));
viewIntent.setDataAndType(uri, mimeType);
if (Build.VERSION.SDK_INT < 19) {
viewIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
viewIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
}
if (context.getPackageManager().queryIntentActivities(viewIntent, 0).size() > 0) {
buttonPrimaryView.setText(R.string.menu_open);
buttonPrimaryView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
context.startActivity(viewIntent);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
}
}
});
} else {
buttonPrimaryView.setVisibility(View.GONE);
}
} else {
buttonPrimaryView.setVisibility(View.GONE);
}
} else {
buttonPrimaryView.setVisibility(View.GONE);
}

View File

@ -6,7 +6,6 @@ import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Schema;
@ -58,4 +57,13 @@ class InstalledAppListAdapter extends RecyclerView.Adapter<InstalledAppListItemC
this.cursor = cursor;
notifyDataSetChanged();
}
@Nullable
public App getItem(int position) {
if (cursor == null) {
return null;
}
cursor.moveToPosition(position);
return new App(cursor);
}
}

View File

@ -22,17 +22,20 @@ package org.fdroid.fdroid.views.installed;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.app.ShareCompat;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema;
@ -100,4 +103,35 @@ public class InstalledAppsActivity extends AppCompatActivity implements LoaderMa
adapter.setApps(null);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.installed_apps, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_share:
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("packageName,versionCode,versionName\n");
for (int i = 0; i < adapter.getItemCount(); i++) {
App app = adapter.getItem(i);
if (app != null) {
stringBuilder.append(app.packageName).append(',')
.append(app.installedVersionCode).append(',')
.append(app.installedVersionName).append('\n');
}
}
ShareCompat.IntentBuilder intentBuilder = ShareCompat.IntentBuilder.from(this)
.setSubject(getString(R.string.send_installed_apps))
.setChooserTitle(R.string.send_installed_apps)
.setText(stringBuilder.toString())
.setType("text/csv");
startActivity(intentBuilder.getIntent());
break;
}
return super.onOptionsItemSelected(item);
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_share"
android:icon="@drawable/ic_share_white"
android:title="@string/menu_share"
app:showAsAction="ifRoom"/>
</menu>

View File

@ -110,6 +110,8 @@ This often occurs with apps installed via Google Play or other sources, if they
<string name="app_list__dismiss_downloading_app">Download canceled</string>
<string name="installed_apps__activity_title">Installed Apps</string>
<string name="send_installed_apps">Share installed apps</string>
<string name="send_installed_apps_csv">Apps installed by F-Droid as CSV file</string>
<string name="installed_app__updates_ignored">Updates ignored</string>
<string name="installed_app__updates_ignored_for_suggested_version">Updates ignored for Version %1$s</string>
<!-- The inline download button shown in the "Updates" screen only uses an icon and so requires
@ -171,6 +173,7 @@ This often occurs with apps installed via Google Play or other sources, if they
<string name="menu_add_repo">New Repository</string>
<string name="menu_launch">Run</string>
<string name="menu_open">Open</string>
<string name="menu_share">Share</string>
<string name="menu_install">Install</string>
<string name="menu_uninstall">Uninstall</string>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path
name="install_history"
path="install_history" />
</paths>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="install_history" path="install_history"/>
<external-path name="external" path="/"/>
</paths>