WIP: Serialize bonjour peers correctly. Refactor mini-service APIs.

The reference to mini-services above are not full blown Android
services. Rather, they are utility classes which can be started,
stopped, and send broadcasts about their status.

Made the list of apps to install better, with buttons for install
or upgrade, and statuses for incompatible and installed.
This commit is contained in:
Peter Serwylo 2015-06-28 23:25:16 +10:00
parent 22b072962e
commit a3af6b8b9f
31 changed files with 1384 additions and 411 deletions

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingBottom="2dip"
android:paddingTop="2dip">
<ImageView
android:id="@android:id/icon"
android:layout_width="48dip"
android:layout_height="48dip"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:layout_marginTop="6dip"
android:layout_alignParentStart="true"
tools:src="@drawable/ic_launcher" />
<RelativeLayout
android:id="@+id/button_or_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerInParent="true"
android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/fdroid_green"
android:textColor="@android:color/white"
tools:text="Install"/>
<TextView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/fdroid_green"
tools:text="Installed" />
</RelativeLayout>
<TextView
android:id="@+id/name"
android:layout_toEndOf="@android:id/icon"
android:layout_toStartOf="@+id/button_or_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:layout_marginTop="6dip"
android:layout_marginBottom="6dip"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="F-Droid" />
</RelativeLayout>

View File

@ -7,196 +7,209 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/header"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="130dp">
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
<RelativeLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/swap_start_header"/>
android:layout_height="130dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_info_white"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="10dp"
android:layout_marginTop="10dp"
/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/swap_start_header"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:padding="20dp"
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:textAlignment="center"
android:text="@string/swap_intro"
android:textColor="@color/white"
android:textSize="18sp" />
</RelativeLayout>
<LinearLayout
android:id="@+id/bluetooth_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:weightSum="1">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:tint="@color/swap_grey_icon"
android:src="@drawable/ic_bluetooth_white" />
<TextView
android:id="@+id/bluetooth_visible"
android:paddingStart="15dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/swap_visible_bluetooth"
android:textSize="18sp" />
<TextView
android:id="@+id/device_id_bluetooth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="SP-120"
android:textColor="@color/swap_light_text"
android:layout_weight="1.00"
android:paddingStart="10dp"
android:paddingEnd="10dp" />
<Switch
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:checked="true"
android:id="@+id/switch_bluetooth" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:weightSum="1">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:tint="@color/swap_grey_icon"
android:src="@drawable/ic_network_wifi_white" />
<LinearLayout
android:paddingStart="15dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_info_white"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="10dp"
android:layout_marginTop="10dp"
/>
<TextView
android:id="@+id/wifi_visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/swap_not_visible_wifi"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:padding="20dp"
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:textAlignment="center"
android:text="@string/swap_intro"
android:textColor="@color/white"
android:textSize="18sp" />
<TextView
android:id="@+id/wifi_network"
</RelativeLayout>
<LinearLayout
android:id="@+id/bluetooth_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:tint="@color/swap_grey_icon"
android:src="@drawable/ic_bluetooth_white" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="15dp"
android:layout_weight="1.00">
<TextView
android:id="@+id/bluetooth_visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/swap_visible_bluetooth"
android:textSize="18sp" />
<TextView
android:id="@+id/device_id_bluetooth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="SP-120"
android:textColor="@color/swap_light_text" />
</LinearLayout>
<Switch
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="wifi network name"
android:textColor="@color/swap_bright_blue"
android:textSize="16sp" />
tools:checked="true"
android:id="@+id/switch_bluetooth" />
</LinearLayout>
<TextView
android:id="@+id/device_id_wifi"
android:layout_width="0dp"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/swap_wifi_device_name"
android:textColor="@color/swap_light_text"
android:layout_weight="1.00"
android:paddingStart="10dp"
android:paddingEnd="10dp" />
android:padding="10dp"
android:orientation="horizontal">
<Switch
android:layout_width="wrap_content"
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:tint="@color/swap_grey_icon"
android:src="@drawable/ic_network_wifi_white" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="15dp"
android:layout_weight="1.00">
<TextView
android:id="@+id/wifi_visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/swap_not_visible_wifi"
android:textSize="18sp" />
<TextView
android:id="@+id/device_id_wifi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/swap_wifi_device_name"
android:textColor="@color/swap_light_text" />
<TextView
android:id="@+id/wifi_network"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="wifi network name"
android:textColor="@color/swap_bright_blue"
android:textSize="16sp" />
</LinearLayout>
<Switch
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:checked="false"
android:id="@+id/switch_wifi" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:checked="false"
android:id="@+id/switch_wifi" />
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingBottom="5dp"
android:paddingTop="20dp">
</LinearLayout>
<TextView
android:id="@+id/text_people_nearby"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/swap_people_nearby"
android:textColor="@color/swap_light_text"
android:layout_weight="1.00"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingBottom="5dp"
android:paddingTop="20dp">
<ProgressBar
android:id="@+id/searching_people_nearby"
android:layout_width="24dp"
android:layout_height="24dp"
android:indeterminate="true" />
</LinearLayout>
<ListView
android:id="@+id/list_people_nearby"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</ListView>
<TextView
android:id="@+id/text_people_nearby"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/swap_people_nearby"
android:textColor="@color/swap_light_text"
android:layout_weight="1.00"/>
<ProgressBar
android:id="@+id/searching_people_nearby"
android:layout_width="24dp"
android:layout_height="24dp"
android:indeterminate="true" />
</LinearLayout>
<ListView
android:id="@+id/list_people_nearby"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</ListView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/swap_cant_find_peers"
android:paddingStart="20dp"
android:textColor="@color/swap_light_text" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/btn_send_fdroid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableStart="@drawable/ic_fdroid_grey"
android:text="@string/swap_send_fdroid"
android:drawablePadding="10dp"
android:padding="25dp"
android:background="@android:color/transparent" />
android:text="@string/swap_cant_find_peers"
android:paddingStart="20dp"
android:paddingTop="20dp"
android:textColor="@color/swap_light_text" />
<Button
android:id="@+id/btn_qr_scanner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableStart="@drawable/ic_qr_grey"
android:text="@string/swap_scan_qr_code"
android:drawablePadding="10dp"
android:padding="25dp"
android:background="@android:color/transparent" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/btn_send_fdroid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableStart="@drawable/ic_fdroid_grey"
android:text="@string/swap_send_fdroid"
android:drawablePadding="10dp"
android:paddingStart="25dp"
android:paddingEnd="25dp"
android:background="@android:color/transparent" />
<Button
android:id="@+id/btn_qr_scanner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableStart="@drawable/ic_qr_grey"
android:text="@string/swap_scan_qr_code"
android:drawablePadding="10dp"
android:paddingStart="25dp"
android:paddingEnd="25dp"
android:background="@android:color/transparent" />
</LinearLayout>
</LinearLayout>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
<org.fdroid.fdroid.views.swap.ConfirmReceive
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
@ -43,31 +43,34 @@
<!-- 46px * 0.56 = 25.76sp -->
<LinearLayout
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_below="@+id/text_description"
android:layout_centerInParent="true"
android:layout_marginTop="45dp">
<!-- 80px * 0.56 = 45dp -->
<!-- TODO: Remove associated style files style="@style/SwapTheme.Wizard.ReceiveSwap.Deny"-->
<Button
android:id="@+id/no_button"
android:text="@string/no"
style="@style/SwapTheme.Wizard.ReceiveSwap.Deny"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="@color/swap_deny"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="25dp"
android:layout_marginRight="25dp"
/>
<!-- TODO: Remove associated style files style="@style/SwapTheme.Wizard.ReceiveSwap.Confirm" -->
<Button
android:id="@+id/yes_button"
android:text="@string/yes"
style="@style/SwapTheme.Wizard.ReceiveSwap.Confirm"
android:layout_width="0dp"
android:layout_weight="1"/>
android:background="@color/swap_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
</RelativeLayout>
</org.fdroid.fdroid.views.swap.ConfirmReceive>

View File

@ -3,15 +3,15 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="?attr/listPreferredItemHeight"
android:orientation="horizontal"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingBottom="2dip"
android:paddingTop="2dip">
android:minHeight="?attr/listPreferredItemHeight"
android:paddingBottom="4dip"
android:paddingTop="4dip">
<RelativeLayout
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginLeft="?attr/listPreferredItemPaddingLeft"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:layout_gravity="center_vertical">

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<org.fdroid.fdroid.views.swap.SwapSuccessView
<org.fdroid.fdroid.views.swap.SwapAppsView
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
</org.fdroid.fdroid.views.swap.SwapSuccessView>
</org.fdroid.fdroid.views.swap.SwapAppsView>

View File

@ -0,0 +1,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search_white"
android:title="@string/menu_search"
android:titleCondensed="@string/menu_search"/>
</menu>

View File

@ -54,6 +54,7 @@
like
this: https://f-droid.org/repo</string>
<string name="incompatible">Incompatible</string>
<string name="inst">Installed</string>
<string name="not_inst">Not Installed</string>
<string name="inst_known_source">Installed (from %s)</string>
@ -336,6 +337,8 @@
<string name="swap_send_fdroid">SEND F-DROID</string>
<string name="swap_scan_qr_code">SCAN QR CODE</string>
<string name="swap_no_peers_nearby">Could not find people nearby to swap with.</string>
<string name="swap_connecting">Connecting</string>
<string name="swap_confirm">Confirm swap</string>
<!-- WiFi AP status for Swap flow -->
<string name="wifi_ap_public">Public</string>
@ -344,5 +347,4 @@
<string name="wifi_warning_public">May work</string>
<string name="wifi_warning_private">Promising</string>
<string name="wifi_warning_personal">Best bet</string>
<string name="swap_connecting">Connecting</string>
</resources>

View File

@ -0,0 +1,100 @@
package javax.jmdns.impl;
import android.os.Parcel;
import android.os.Parcelable;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import javax.jmdns.ServiceInfo;
/**
* The ServiceInfo class needs to be serialized in order to be sent as an Android broadcast.
* In order to make it Parcelable (or Serializable for that matter), there are some package-scope
* methods which needed to be used. Thus, this class is in the javax.jmdns.impl package so that
* it can access those methods. This is as an alternative to modifying the source code of JmDNS.
*/
public class FDroidServiceInfo extends ServiceInfoImpl implements Parcelable {
public FDroidServiceInfo(ServiceInfo info) {
super(info);
}
private static byte[] readBytes(Parcel in) {
byte[] bytes = new byte[in.readInt()];
in.readByteArray(bytes);
return bytes;
}
public FDroidServiceInfo(Parcel in) {
super(
in.readString(),
in.readString(),
in.readString(),
in.readInt(),
in.readInt(),
in.readInt(),
in.readByte() != 0,
readBytes(in)
);
int addressCount = in.readInt();
for (int i = 0; i < addressCount; i ++) {
try {
addAddress((Inet4Address)Inet4Address.getByAddress(readBytes(in)));
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
addressCount = in.readInt();
for (int i = 0; i < addressCount; i ++) {
try {
addAddress((Inet6Address)Inet6Address.getByAddress(readBytes(in)));
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(getType());
dest.writeString(getName());
dest.writeString(getSubtype());
dest.writeInt(getPort());
dest.writeInt(getWeight());
dest.writeInt(getPriority());
dest.writeByte(isPersistent() ? (byte) 1 : (byte) 0);
dest.writeInt(getTextBytes().length);
dest.writeByteArray(getTextBytes());
dest.writeInt(getInet4Addresses().length);
for (int i = 0; i < getInet4Addresses().length; i ++) {
Inet4Address address = getInet4Addresses()[i];
dest.writeInt(address.getAddress().length);
dest.writeByteArray(address.getAddress());
}
dest.writeInt(getInet6Addresses().length);
for (int i = 0; i < getInet6Addresses().length; i ++) {
Inet6Address address = getInet6Addresses()[i];
dest.writeInt(address.getAddress().length);
dest.writeByteArray(address.getAddress());
}
}
public static final Parcelable.Creator<FDroidServiceInfo> CREATOR = new Parcelable.Creator<FDroidServiceInfo>() {
public FDroidServiceInfo createFromParcel(Parcel source) {
return new FDroidServiceInfo(source);
}
public FDroidServiceInfo[] newArray(int size) {
return new FDroidServiceInfo[size];
}
};
}

View File

@ -40,6 +40,7 @@ import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.TextUtils;
@ -110,13 +111,14 @@ public class UpdateService extends IntentService implements ProgressListener {
// update in response to a user request.
public static class UpdateReceiver extends ResultReceiver {
private Context context;
private final Context context;
private ProgressDialog dialog;
private ProgressListener listener;
private String lastShownMessage = null;
public UpdateReceiver(Handler handler) {
public UpdateReceiver(@NonNull Context context, Handler handler) {
super(handler);
this.context = context;
}
public UpdateReceiver setDialog(ProgressDialog dialog) {
@ -145,8 +147,7 @@ public class UpdateService extends IntentService implements ProgressListener {
}
}
public UpdateReceiver showDialog(Context context) {
this.context = context;
public UpdateReceiver showDialog() {
ensureDialog();
dialog.show();
return this;
@ -227,9 +228,9 @@ public class UpdateService extends IntentService implements ProgressListener {
public static UpdateReceiver updateRepoNow(String address, Context context, boolean showDialog) {
Intent intent = new Intent(context, UpdateService.class);
UpdateReceiver receiver = new UpdateReceiver(new Handler());
UpdateReceiver receiver = new UpdateReceiver(context, new Handler());
if (showDialog) {
receiver.showDialog(context);
receiver.showDialog();
}
intent.putExtra(EXTRA_RECEIVER, receiver);
if (!TextUtils.isEmpty(address)) {

View File

@ -193,7 +193,7 @@ public class AppProvider extends FDroidProvider {
}
String[] ALL = {
IS_COMPATIBLE, APP_ID, NAME, SUMMARY, ICON, DESCRIPTION,
_ID, 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,
UPSTREAM_VERSION, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED,
@ -396,6 +396,7 @@ public class AppProvider extends FDroidProvider {
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_SEARCH_REPO = "searchRepo";
private static final String PATH_NO_APKS = "noApks";
private static final String PATH_APPS = "apps";
private static final String PATH_RECENTLY_UPDATED = "recentlyUpdated";
@ -416,6 +417,7 @@ public class AppProvider extends FDroidProvider {
private static final int IGNORED = CATEGORY + 1;
private static final int CALC_APP_DETAILS_FROM_INDEX = IGNORED + 1;
private static final int REPO = CALC_APP_DETAILS_FROM_INDEX + 1;
private static final int SEARCH_REPO = REPO + 1;
static {
matcher.addURI(getAuthority(), null, CODE_LIST);
@ -425,6 +427,7 @@ public class AppProvider extends FDroidProvider {
matcher.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED);
matcher.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY);
matcher.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH);
matcher.addURI(getAuthority(), PATH_SEARCH_REPO + "/*/*", SEARCH_REPO);
matcher.addURI(getAuthority(), PATH_REPO + "/#", REPO);
matcher.addURI(getAuthority(), PATH_CAN_UPDATE, CAN_UPDATE);
matcher.addURI(getAuthority(), PATH_INSTALLED, INSTALLED);
@ -508,6 +511,14 @@ public class AppProvider extends FDroidProvider {
.build();
}
public static Uri getSearchUri(Repo repo, String query) {
return getContentUri().buildUpon()
.appendPath(PATH_SEARCH_REPO)
.appendPath(repo.id + "")
.appendPath(query)
.build();
}
@Override
protected String getTableName() { return DBHelper.TABLE_APP; }
@ -680,6 +691,11 @@ public class AppProvider extends FDroidProvider {
includeSwap = false;
break;
case SEARCH_REPO:
selection = selection.add(querySearch(uri.getPathSegments().get(1)));
selection = selection.add(queryRepo(Long.parseLong(uri.getPathSegments().get(0))));
break;
case NO_APKS:
selection = selection.add(queryNoApks());
break;

View File

@ -1,13 +1,14 @@
package org.fdroid.fdroid.localrepo;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.IBinder;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
@ -16,8 +17,10 @@ import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.localrepo.peers.Peer;
import java.lang.annotation.Retention;
@ -116,12 +119,14 @@ public class SwapManager {
// ("Step" refers to the current view being shown in the UI)
// ==========================================================
public static final int STEP_INTRO = 1;
public static final int STEP_SELECT_APPS = 2;
public static final int STEP_JOIN_WIFI = 3;
public static final int STEP_SHOW_NFC = 4;
public static final int STEP_WIFI_QR = 5;
public static final int STEP_CONNECTING = 6;
public static final int STEP_INTRO = 1;
public static final int STEP_SELECT_APPS = 2;
public static final int STEP_JOIN_WIFI = 3;
public static final int STEP_SHOW_NFC = 4;
public static final int STEP_WIFI_QR = 5;
public static final int STEP_CONNECTING = 6;
public static final int STEP_SUCCESS = 7;
public static final int STEP_CONFIRM_SWAP = 8;
private @SwapStep int step = STEP_INTRO;
@ -143,13 +148,108 @@ public class SwapManager {
return appsToSwap;
}
@Nullable
public UpdateService.UpdateReceiver refreshSwap() {
return this.peer != null ? connectTo(peer) : null;
}
@NonNull
public UpdateService.UpdateReceiver connectTo(@NonNull Peer peer) {
if (peer != this.peer) {
Log.e(TAG, "Oops, got a different peer to swap with than initially planned.");
}
peerRepo = ensureRepoExists(peer);
// Only ask server to swap with us, if we are actually running a local repo service.
// It is possible to have a swap initiated without first starting a swap, in which
// case swapping back is pointless.
/*if (!newRepoConfig.preventFurtherSwaps() && isEnabled()) {
askServerToSwapWithUs();
}*/
return UpdateService.updateRepoNow(peer.getRepoAddress(), context, false);
}
/*
private void askServerToSwapWithUs() {
if (!newRepoConfig.isValidRepo()) {
return;
}
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... args) {
Uri repoUri = newRepoConfig.getRepoUri();
String swapBackUri = Utils.getLocalRepoUri(FDroidApp.repo).toString();
AndroidHttpClient client = AndroidHttpClient.newInstance("F-Droid", ConnectSwapActivity.this);
HttpPost request = new HttpPost("/request-swap");
HttpHost host = new HttpHost(repoUri.getHost(), repoUri.getPort(), repoUri.getScheme());
try {
Log.d(TAG, "Asking server at " + newRepoConfig.getRepoUriString() + " to swap with us in return (by POSTing to \"/request-swap\" with repo \"" + swapBackUri + "\")...");
populatePostParams(swapBackUri, request);
client.execute(host, request);
} catch (IOException e) {
notifyOfErrorOnUiThread();
Log.e(TAG, "Error while asking server to swap with us: " + e.getMessage());
} finally {
client.close();
}
return null;
}
private void populatePostParams(String swapBackUri, HttpPost request) throws UnsupportedEncodingException {
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("repo", swapBackUri));
UrlEncodedFormEntity encodedParams = new UrlEncodedFormEntity(params);
request.setEntity(encodedParams);
}
private void notifyOfErrorOnUiThread() {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(
ConnectSwapActivity.this,
R.string.swap_reciprocate_failed,
Toast.LENGTH_LONG
).show();
}
});
}
}.execute();
}*/
private Repo ensureRepoExists(@NonNull Peer peer) {
// TODO: newRepoConfig.getParsedUri() will include a fingerprint, which may not match with
// the repos address in the database. Not sure on best behaviour in this situation.
Repo repo = RepoProvider.Helper.findByAddress(context, peer.getRepoAddress());
if (repo == null) {
ContentValues values = new ContentValues(6);
// TODO: i18n and think about most appropriate name. Although it wont be visible in
// the "Manage repos" UI after being marked as a swap repo here...
values.put(RepoProvider.DataColumns.NAME, peer.getName());
values.put(RepoProvider.DataColumns.ADDRESS, peer.getRepoAddress());
values.put(RepoProvider.DataColumns.DESCRIPTION, ""); // TODO;
values.put(RepoProvider.DataColumns.FINGERPRINT, peer.getFingerprint());
values.put(RepoProvider.DataColumns.IN_USE, true);
values.put(RepoProvider.DataColumns.IS_SWAP, true);
Uri uri = RepoProvider.Helper.insert(context, values);
repo = RepoProvider.Helper.findByUri(context, uri);
}
return repo;
}
@Nullable
public Repo getPeerRepo() {
return peerRepo;
}
public void install(@NonNull final App app) {
}
/**
* Ensure that we don't get put into an incorrect state, by forcing people to pass valid
@ -158,7 +258,7 @@ public class SwapManager {
* This is the same as, e.g. {@link Context#getSystemService(String)}
*/
@IntDef({STEP_INTRO, STEP_SELECT_APPS, STEP_JOIN_WIFI, STEP_SHOW_NFC, STEP_WIFI_QR,
STEP_CONNECTING})
STEP_CONNECTING, STEP_SUCCESS, STEP_CONFIRM_SWAP})
@Retention(RetentionPolicy.SOURCE)
public @interface SwapStep {}
@ -171,6 +271,9 @@ public class SwapManager {
@Nullable
private Peer peer;
@Nullable
private Repo peerRepo;
public void swapWith(Peer peer) {
this.peer = peer;
}
@ -313,43 +416,19 @@ public class SwapManager {
// Interacting with Bluetooth adapter
// ==========================================
@Nullable /* Emulators tend not to have bluetooth adapters. */
private final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
public boolean isBluetoothDiscoverable() {
return bluetooth != null &&
bluetooth.getScanMode() == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
return service != null && service.getBluetooth().isConnected();
}
public void ensureBluetoothDiscoverable() {
if (bluetooth == null) {
return;
}
if (!bluetooth.isEnabled()) {
if (!bluetooth.enable()) {
}
}
if (bluetooth.isEnabled()) {
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0);
// TODO: Hmm, don't like the idea of a background service being able to do this :(
discoverableIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(discoverableIntent);
if (service != null) {
service.getBluetooth().start();
}
}
public void makeBluetoothNonDiscoverable() {
if (bluetooth == null) {
return;
}
if (isBluetoothDiscoverable()) {
// TODO: How to disable this?
if (service != null) {
service.getBluetooth().stop();
}
}
@ -361,6 +440,18 @@ public class SwapManager {
return isWifiConnected() && service != null && service.isEnabled();
}
public void ensureBonjourDiscoverable() {
if (!isBonjourDiscoverable()) {
// TODO: Enable bonjour (currently it is enabled by default when the service starts)
}
}
public void makeBonjourNotDiscoverable() {
if (service != null) {
// TODO: Disable bonjour (currently it is enabled by default when the service starts)
}
}
public boolean isScanningForPeers() {
return service != null && service.isScanningForPeers();
}

View File

@ -18,8 +18,9 @@ import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.localrepo.peers.BluetoothFinder;
import org.fdroid.fdroid.localrepo.peers.BonjourFinder;
import org.fdroid.fdroid.localrepo.type.BluetoothType;
import org.fdroid.fdroid.localrepo.type.BonjourType;
import org.fdroid.fdroid.localrepo.type.NfcType;
import org.fdroid.fdroid.localrepo.type.SwapType;
import org.fdroid.fdroid.localrepo.type.WebServerType;
import org.fdroid.fdroid.net.WifiStateChangeService;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
@ -31,27 +32,26 @@ import java.util.TimerTask;
* Central service which manages all of the different moving parts of swap which are required
* to enable p2p swapping of apps. Currently manages WiFi and NFC. Will manage Bluetooth in
* the future.
*
* TODO: Manage threading correctly.
*/
public class SwapService extends Service {
private static final String TAG = "SwapService";
public static final String BONJOUR_STATE_CHANGE = "org.fdroid.fdroid.BONJOUR_STATE_CHANGE";
public static final String BLUETOOTH_STATE_CHANGE = "org.fdroid.fdroid.BLUETOOTH_STATE_CHANGE";
public static final String EXTRA_STARTING = "STARTING";
public static final String EXTRA_STARTED = "STARTED";
public static final String EXTRA_STOPPED = "STOPPED";
private static final int NOTIFICATION = 1;
private final Binder binder = new Binder();
private final BonjourType bonjourType;
private final WebServerType webServerType;
private SwapType bonjourType;
private SwapType bluetoothType;
private SwapType webServerType;
private final BonjourFinder bonjourFinder;
private final BluetoothFinder bluetoothFinder;
// TODO: The NFC type can't really be managed by the service, because it is intrinsically tied
// to a specific _Activity_, and will only be active while that activity is shown. This service
// knows nothing about activities.
private final NfcType nfcType;
private BonjourFinder bonjourFinder;
private BluetoothFinder bluetoothFinder;
private final static int TIMEOUT = 900000; // 15 mins
@ -65,22 +65,29 @@ public class SwapService extends Service {
return bonjourFinder.isScanning() || bluetoothFinder.isScanning();
}
public SwapType getBluetooth() {
return bluetoothType;
}
public SwapType getBonjour() {
return bluetoothType;
}
public class Binder extends android.os.Binder {
public SwapService getService() {
return SwapService.this;
}
}
public SwapService() {
nfcType = new NfcType(this);
public void onCreate() {
super.onCreate();
bonjourType = new BonjourType(this);
bluetoothType = BluetoothType.create(this);
webServerType = new WebServerType(this);
bonjourFinder = new BonjourFinder(this);
bluetoothFinder = new BluetoothFinder(this);
}
public void onCreate() {
super.onCreate();
Log.d(TAG, "Creating service, will register appropriate listeners.");
Preferences.get().registerLocalRepoBonjourListeners(bonjourEnabledListener);
Preferences.get().registerLocalRepoHttpsListeners(httpsEnabledListener);
@ -139,7 +146,7 @@ public class SwapService extends Service {
@Override
protected Void doInBackground(Void... params) {
Log.d(TAG, "Started background task to enable swapping.");
enableSwappingSynchronous();
enableSwappingAsynchronous();
return null;
}
@ -160,13 +167,13 @@ public class SwapService extends Service {
/**
* The guts of this class - responsible for enabling the relevant services for swapping.
* * Doesn't know anything about enabled/disabled.
* * Runs synchronously on the thread it was called.
* Doesn't know anything about enabled/disabled state, you should check that before invoking
* this method so it doesn't start something that is already started.
* Runs asynchronously on several background threads.
*/
private void enableSwappingSynchronous() {
nfcType.start();
webServerType.start();
bonjourType.start();
private void enableSwappingAsynchronous() {
webServerType.startInBackground();
bonjourType.startInBackground();
}
public void disableSwapping() {
@ -199,13 +206,12 @@ public class SwapService extends Service {
}
/**
* @see SwapService#enableSwappingSynchronous()
* @see SwapService#enableSwappingAsynchronous()
*/
private void disableSwappingSynchronous() {
Log.d(TAG, "Disabling SwapService (bonjour, webserver, etc)");
bonjourType.stop();
webServerType.stop();
nfcType.stop();
}
public boolean isEnabled() {
@ -219,7 +225,7 @@ public class SwapService extends Service {
protected Void doInBackground(Void... params) {
Log.d(TAG, "Restarting swap services.");
disableSwappingSynchronous();
enableSwappingSynchronous();
enableSwappingAsynchronous();
return null;
}
}.execute();
@ -245,6 +251,7 @@ public class SwapService extends Service {
}
@SuppressWarnings("FieldCanBeLocal") // The constructor will get bloated if these are all local...
// TODO: Remove this preference...
private final Preferences.ChangeListener bonjourEnabledListener = new Preferences.ChangeListener() {
@Override
public void onPreferenceChange() {

View File

@ -30,7 +30,7 @@ public class BluetoothPeer implements Peer {
}
@Override
public boolean equals(Peer peer) {
public boolean equals(Object peer) {
return peer != null && peer instanceof BluetoothPeer && ((BluetoothPeer)peer).device.getAddress().equals(device.getAddress());
}
@ -39,6 +39,12 @@ public class BluetoothPeer implements Peer {
return "bluetooth://" + device.getAddress() + "/fdroid/repo";
}
@Override
public String getFingerprint() {
// TODO: Get fingerprint somehow over bluetooth.
return "";
}
@Override
public int describeContents() {
return 0;

View File

@ -5,12 +5,14 @@ import android.net.wifi.WifiManager;
import android.os.AsyncTask;
import android.util.Log;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.localrepo.LocalRepoKeyStore;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.util.Timer;
import java.util.TimerTask;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceEvent;
@ -24,7 +26,7 @@ public class BonjourFinder extends PeerFinder<BonjourPeer> implements ServiceLis
public static final String HTTP_SERVICE_TYPE = "_http._tcp.local.";
public static final String HTTPS_SERVICE_TYPE = "_https._tcp.local.";
private JmDNS mJmdns;
private JmDNS jmdns;
private WifiManager wifiManager;
private WifiManager.MulticastLock mMulticastLock;
@ -35,6 +37,8 @@ public class BonjourFinder extends PeerFinder<BonjourPeer> implements ServiceLis
@Override
public void scan() {
Log.d(TAG, "Requested Bonjour (mDNS) scan for peers.");
if (wifiManager == null) {
wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
mMulticastLock = wifiManager.createMulticastLock(context.getPackageName());
@ -42,7 +46,8 @@ public class BonjourFinder extends PeerFinder<BonjourPeer> implements ServiceLis
}
if (isScanning) {
Log.d(TAG, "Requested Bonjour scan, but already scanning, so will ignore request.");
Log.d(TAG, "Requested Bonjour scan, but already scanning. But we will still try to explicitly scan for services.");
// listServices();
return;
}
@ -53,16 +58,8 @@ public class BonjourFinder extends PeerFinder<BonjourPeer> implements ServiceLis
@Override
protected Void doInBackground(Void... params) {
try {
int ip = wifiManager.getConnectionInfo().getIpAddress();
byte[] byteIp = {
(byte) (ip & 0xff),
(byte) (ip >> 8 & 0xff),
(byte) (ip >> 16 & 0xff),
(byte) (ip >> 24 & 0xff)
};
Log.d(TAG, "Searching for mDNS clients...");
mJmdns = JmDNS.create(InetAddress.getByAddress(byteIp));
Log.d(TAG, "Finished searching for mDNS clients.");
Log.d(TAG, "Searching for Bonjour (mDNS) clients...");
jmdns = JmDNS.create(InetAddress.getByName(FDroidApp.ipAddressString));
} catch (IOException e) {
e.printStackTrace();
}
@ -71,16 +68,44 @@ public class BonjourFinder extends PeerFinder<BonjourPeer> implements ServiceLis
@Override
protected void onPostExecute(Void result) {
Log.d(TAG, "Adding mDNS service listeners.");
if (mJmdns != null) {
mJmdns.addServiceListener(HTTP_SERVICE_TYPE, BonjourFinder.this);
mJmdns.addServiceListener(HTTPS_SERVICE_TYPE, BonjourFinder.this);
if (jmdns != null) {
Log.d(TAG, "Adding mDNS service listeners for " + HTTP_SERVICE_TYPE + " and " + HTTPS_SERVICE_TYPE);
jmdns.addServiceListener(HTTP_SERVICE_TYPE, BonjourFinder.this);
jmdns.addServiceListener(HTTPS_SERVICE_TYPE, BonjourFinder.this);
listServices();
}
}
}.execute();
}
private void listServices() {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
Log.d(TAG, "Explicitly querying for services, in addition to waiting for notifications.");
addFDroidServices(jmdns.list(HTTP_SERVICE_TYPE));
addFDroidServices(jmdns.list(HTTPS_SERVICE_TYPE));
return null;
}
// TODO: Remove once stable, added here for testing because it is easier to see the
// data being broadcast over mDNS.
/*@Override
protected void onPostExecute(Void v) {
Log.d(TAG, "Queuing up another poll in 2 secs.");
new Timer().schedule(new TimerTask() {
@Override
public void run() {
Log.d(TAG, "Time to poll for services again.");
listServices();
}
}, 2000);
}*/
}.execute();
}
@Override
public void serviceRemoved(ServiceEvent event) {
}
@ -95,11 +120,11 @@ public class BonjourFinder extends PeerFinder<BonjourPeer> implements ServiceLis
// so that more detailed info can be shown to the user.
//
// If so, when is the old one removed?
addFDroidService(event);
addFDroidService(event.getInfo());
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
mJmdns.requestServiceInfo(event.getType(), event.getName(), true);
jmdns.requestServiceInfo(event.getType(), event.getName(), true);
return null;
}
}.execute();
@ -107,7 +132,13 @@ public class BonjourFinder extends PeerFinder<BonjourPeer> implements ServiceLis
@Override
public void serviceResolved(ServiceEvent event) {
addFDroidService(event);
addFDroidService(event.getInfo());
}
private void addFDroidServices(ServiceInfo[] services) {
for (ServiceInfo info : services) {
addFDroidService(info);
}
}
/**
@ -115,14 +146,24 @@ public class BonjourFinder extends PeerFinder<BonjourPeer> implements ServiceLis
* Checks that the service is an F-Droid service, and also that it is not the F-Droid service
* for this device (by comparing its signing fingerprint to our signing fingerprint).
*/
private void addFDroidService(ServiceEvent event) {
final ServiceInfo serviceInfo = event.getInfo();
private void addFDroidService(ServiceInfo serviceInfo) {
final String type = serviceInfo.getPropertyString("type");
final String fingerprint = serviceInfo.getPropertyString("fingerprint");
final boolean isFDroid = type != null && type.startsWith("fdroidrepo");
final boolean isSelf = FDroidApp.repo != null && fingerprint != null && fingerprint.equalsIgnoreCase(FDroidApp.repo.fingerprint);
if (isFDroid && !isSelf) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Found F-Droid swap Bonjour service:\n" + serviceInfo);
}
foundPeer(new BonjourPeer(serviceInfo));
} else {
if (BuildConfig.DEBUG) {
if (isSelf) {
Log.d(TAG, "Ignoring Bonjour service because it belongs to this device:\n" + serviceInfo);
} else {
Log.d(TAG, "Ignoring Bonjour service because it doesn't look like an F-Droid swap repo:\n" + serviceInfo);
}
}
}
}
@ -132,13 +173,14 @@ public class BonjourFinder extends PeerFinder<BonjourPeer> implements ServiceLis
mMulticastLock.release();
}
if (mJmdns == null)
return;
mJmdns.removeServiceListener(HTTP_SERVICE_TYPE, this);
mJmdns.removeServiceListener(HTTPS_SERVICE_TYPE, this);
mJmdns = null;
isScanning = false;
if (jmdns == null)
return;
jmdns.removeServiceListener(HTTP_SERVICE_TYPE, this);
jmdns.removeServiceListener(HTTPS_SERVICE_TYPE, this);
jmdns = null;
}
}

View File

@ -4,14 +4,19 @@ import android.os.Parcel;
import org.fdroid.fdroid.R;
import java.net.Inet4Address;
import java.net.Inet6Address;
import javax.jmdns.impl.FDroidServiceInfo;
import javax.jmdns.ServiceInfo;
import javax.jmdns.impl.ServiceInfoImpl;
public class BonjourPeer implements Peer {
private ServiceInfo serviceInfo;
private FDroidServiceInfo serviceInfo;
public BonjourPeer(ServiceInfo serviceInfo) {
this.serviceInfo = serviceInfo;
this.serviceInfo = new FDroidServiceInfo(serviceInfo);
}
@Override
@ -30,18 +35,22 @@ public class BonjourPeer implements Peer {
}
@Override
public boolean equals(Peer peer) {
public boolean equals(Object peer) {
if (peer != null && peer instanceof BonjourPeer) {
BonjourPeer that = (BonjourPeer)peer;
// TODO: Don't use "name" for comparing, but rather fingerprint of the swap repo.
return that.serviceInfo.getName().equals(this.serviceInfo.getName());
return this.getFingerprint().equals(that.getFingerprint());
}
return false;
}
@Override
public String getRepoAddress() {
return serviceInfo.getURL();
return serviceInfo.getURL(); // Automatically appends the "path" property if present, so no need to do it ourselves.
}
@Override
public String getFingerprint() {
return serviceInfo.getPropertyString("fingerprint");
}
@Override
@ -51,27 +60,11 @@ public class BonjourPeer implements Peer {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(serviceInfo.getType());
dest.writeString(serviceInfo.getName());
dest.writeString(serviceInfo.getSubtype());
dest.writeInt(serviceInfo.getPort());
dest.writeInt(serviceInfo.getWeight());
dest.writeInt(serviceInfo.getPriority());
dest.writeByte(serviceInfo.isPersistent() ? (byte) 1 : (byte) 0);
dest.writeString(serviceInfo.getTextString());
dest.writeParcelable(serviceInfo, flags);
}
protected BonjourPeer(Parcel in) {
String type = in.readString();
String name = in.readString();
String subtype = in.readString();
int port = in.readInt();
int weight = in.readInt();
int priority = in.readInt();
boolean persistent = in.readByte() != 0;
String text = in.readString();
this.serviceInfo = ServiceInfo.create(type, name, subtype, port, weight, priority, persistent, text);
this.serviceInfo = in.readParcelable(FDroidServiceInfo.class.getClassLoader());
}
public static final Creator<BonjourPeer> CREATOR = new Creator<BonjourPeer>() {

View File

@ -9,7 +9,9 @@ public interface Peer extends Parcelable {
@DrawableRes int getIcon();
boolean equals(Peer peer);
boolean equals(Object peer);
String getRepoAddress();
String getFingerprint();
}

View File

@ -37,4 +37,8 @@ public abstract class PeerFinder<T extends Peer> {
context.sendBroadcast(intent);
}
protected void removePeer(T peer) {
// TODO: Broadcast messages when peers are removed too.
}
}

View File

@ -0,0 +1,102 @@
package org.fdroid.fdroid.localrepo.type;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.support.annotation.NonNull;
import android.util.Log;
import org.fdroid.fdroid.localrepo.SwapService;
public class BluetoothType extends SwapType {
private static final String TAG = "BluetoothBroadcastType";
@NonNull
private final BluetoothAdapter adapter;
public static SwapType create(@NonNull Context context) {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
return new NoBluetoothType(context);
} else {
return new BluetoothType(context, adapter);
}
};
private BluetoothType(@NonNull Context context, @NonNull BluetoothAdapter adapter) {
super(context);
this.adapter = adapter;
context.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1)) {
case BluetoothAdapter.SCAN_MODE_NONE:
setConnected(false);
break;
case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE:
setConnected(true);
break;
// Only other is BluetoothAdapter.SCAN_MODE_CONNECTABLE. For now don't handle that.
}
}
}, new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));
}
@Override
public void start() {
sendBroadcast(SwapService.EXTRA_STARTING);
if (!adapter.isEnabled()) {
if (!adapter.enable()) {
setConnected(false);
return;
}
}
if (adapter.isEnabled()) {
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0);
// TODO: Hmm, don't like the idea of a background service being able to do this :(
discoverableIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// Can't get notified if they cancel this, because we are not an activity and thus
// can't start an activity for a result :(
context.startActivity(discoverableIntent);
// Don't setConnected(true) yet, wait for the broadcast to come from the BluetoothAdapter
// saying its state has changed.
}
}
@Override
public void stop() {
Log.d(TAG, "Making bluetooth non-discoverable.");
adapter.cancelDiscovery();
setConnected(false);
}
@Override
public String getBroadcastAction() {
return SwapService.BLUETOOTH_STATE_CHANGE;
}
private static class NoBluetoothType extends SwapType {
public NoBluetoothType(@NonNull Context context) {
super(context);
}
@Override
public void start() {}
@Override
public void stop() {}
}
}

View File

@ -3,33 +3,44 @@ package org.fdroid.fdroid.localrepo.type;
import android.content.Context;
import android.util.Log;
import org.fdroid.fdroid.FDroid;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.localrepo.SwapService;
import java.io.IOException;
import java.net.InetAddress;
import java.util.HashMap;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;
public class BonjourType implements SwapType {
/**
* Sends a {@link SwapService#BONJOUR_STATE_CHANGE} broadcasts when starting, started or stopped.
*/
public class BonjourType extends SwapType {
private static final String TAG = "BonjourBroadcastType";
private static final String TAG = "BonjourSwapService";
private JmDNS jmdns;
private ServiceInfo pairService;
private final Context context;
public BonjourType(Context context) {
this.context = context;
super(context);
}
@Override
public void start() {
if (!Preferences.get().isLocalRepoBonjourEnabled())
Log.d(TAG, "Preparing to start Bonjour service.");
if (!Preferences.get().isLocalRepoBonjourEnabled()) {
// TODO: Remove this preference completely, it is now dealt with in the start swap screen.
Log.i(TAG, "Not going to start Bonjour swap service because disabled via preferences.");
return;
}
sendBroadcast(SwapService.EXTRA_STARTING);
/*
* a ServiceInfo can only be registered with a single instance
@ -54,12 +65,14 @@ public class BonjourType implements SwapType {
try {
Log.d(TAG, "Starting bonjour service...");
pairService = ServiceInfo.create(type, repoName, FDroidApp.port, 0, 0, values);
jmdns = JmDNS.create();
jmdns = JmDNS.create(InetAddress.getByName(FDroidApp.ipAddressString));
jmdns.registerService(pairService);
setConnected(true);
Log.d(TAG, "... Bounjour service started.");
} catch (IOException e) {
Log.e(TAG, "Error while registering jmdns service: " + e);
Log.e(TAG, Log.getStackTraceString(e));
setConnected(false);
}
}
@ -67,6 +80,7 @@ public class BonjourType implements SwapType {
public void stop() {
Log.d(TAG, "Unregistering MDNS service...");
clearCurrentMDNSService();
setConnected(false);
}
private void clearCurrentMDNSService() {
@ -82,8 +96,8 @@ public class BonjourType implements SwapType {
}
@Override
public void restart() {
public String getBroadcastAction() {
return SwapService.BONJOUR_STATE_CHANGE;
}
}

View File

@ -1,27 +0,0 @@
package org.fdroid.fdroid.localrepo.type;
import android.content.Context;
public class NfcType implements SwapType {
private final Context context;
public NfcType(Context context) {
this.context = context;
}
@Override
public void start() {
}
@Override
public void stop() {
}
@Override
public void restart() {
}
}

View File

@ -1,11 +1,73 @@
package org.fdroid.fdroid.localrepo.type;
public interface SwapType {
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
void start();
import org.fdroid.fdroid.localrepo.SwapService;
void stop();
/**
* There is lots of common functionality, and a common API among different communication protocols
* associated with the swap process. This includes Bluetooth visability, Bonjour visability,
* and the web server which serves info for swapping. This class provides a common API for
* starting and stopping these services. In addition, it helps with the process of sending broadcast
* intents in response to the thing starting or stopping.
*/
public abstract class SwapType {
void restart();
private boolean isConnected;
@NonNull
protected final Context context;
public SwapType(@NonNull Context context) {
this.context = context;
}
abstract public void start();
abstract public void stop();
protected String getBroadcastAction() {
return null;
}
protected final void setConnected(boolean connected) {
if (connected) {
isConnected = true;
sendBroadcast(SwapService.EXTRA_STARTED);
} else {
isConnected = false;
sendBroadcast(SwapService.EXTRA_STOPPED);
}
}
/**
* Sends either a {@link org.fdroid.fdroid.localrepo.SwapService#EXTRA_STARTING},
* {@link org.fdroid.fdroid.localrepo.SwapService#EXTRA_STARTED} or
* {@link org.fdroid.fdroid.localrepo.SwapService#EXTRA_STOPPED} broadcast.
*/
protected final void sendBroadcast(String extra) {
if (getBroadcastAction() != null) {
Intent intent = new Intent(getBroadcastAction());
intent.putExtra(extra, true);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
}
public final boolean isConnected() {
return isConnected;
}
public void startInBackground() {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
start();
return null;
}
}.execute();
}
}

View File

@ -17,21 +17,21 @@ import java.io.IOException;
import java.net.BindException;
import java.util.Random;
public class WebServerType implements SwapType {
public class WebServerType extends SwapType {
private static final String TAG = "WebServerType";
private Handler webServerThreadHandler = null;
private LocalHTTPD localHttpd;
private final Context context;
public WebServerType(Context context) {
this.context = context;
super(context);
}
@Override
public void start() {
Log.d(TAG, "Preparing swap webserver.");
Runnable webServer = new Runnable() {
// Tell Eclipse this is not a leak because of Looper use.
@SuppressLint("HandlerLeak")
@ -51,7 +51,9 @@ public class WebServerType implements SwapType {
}
};
try {
Log.d(TAG, "Starting swap webserver...");
localHttpd.start();
Log.d(TAG, "Swap webserver started.");
} catch (BindException e) {
int prev = FDroidApp.port;
FDroidApp.port = FDroidApp.port + new Random().nextInt(1111);
@ -79,9 +81,4 @@ public class WebServerType implements SwapType {
webServerThreadHandler.sendMessage(msg);
}
@Override
public void restart() {
}
}

View File

@ -1,28 +0,0 @@
package org.fdroid.fdroid.localrepo.type;
import android.content.Context;
public class WifiType implements SwapType {
private final Context context;
public WifiType(Context context) {
this.context = context;
}
@Override
public void start() {
}
@Override
public void stop() {
}
@Override
public void restart() {
}
}

View File

@ -38,6 +38,7 @@ public class WifiStateChangeService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "WiFi change service started, clearing info about wifi state until we have figured it out again.");
FDroidApp.initWifiSettings();
NetworkInfo ni = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
@ -66,6 +67,7 @@ public class WifiStateChangeService extends Service {
@Override
protected Void doInBackground(Void... params) {
try {
Log.d(TAG, "Checking wifi state (in background thread).");
WifiInfo wifiInfo = null;
wifiState = wifiManager.getWifiState();
@ -85,9 +87,12 @@ public class WifiStateChangeService extends Service {
} else { // a hotspot can be active during WIFI_STATE_UNKNOWN
FDroidApp.ipAddressString = getIpAddressFromNetworkInterface();
}
Thread.sleep(1000);
if (BuildConfig.DEBUG) {
Log.d(TAG, "waiting for an IP address...");
if (FDroidApp.ipAddressString == null) {
Thread.sleep(1000);
if (BuildConfig.DEBUG) {
Log.d(TAG, "waiting for an IP address...");
}
}
}
if (isCancelled()) // can be canceled by a change via WifiStateChangeReceiver
@ -95,6 +100,7 @@ public class WifiStateChangeService extends Service {
if (wifiInfo != null) {
String ssid = wifiInfo.getSSID();
Log.d(TAG, "Have wifi info, connected to " + ssid);
if (ssid != null) {
FDroidApp.ssid = ssid.replaceAll("^\"(.*)\"$", "$1");
}
@ -104,6 +110,7 @@ public class WifiStateChangeService extends Service {
}
}
// TODO: Can this be moved to the swap service instead?
String scheme;
if (Preferences.get().isLocalRepoHttpsEnabled())
scheme = "https";

View File

@ -148,7 +148,7 @@ public class ManageReposActivity extends ActionBarActivity {
protected void onResume() {
super.onResume();
if (updateHandler != null) {
updateHandler.showDialog(this);
updateHandler.showDialog();
}
/* let's see if someone is trying to send us a new repo */
addRepoFromIntent(getIntent());

View File

@ -82,7 +82,7 @@ public class RepoDetailsFragment extends Fragment {
public void onAttach(Activity activity) {
super.onAttach(activity);
if (updateHandler != null) {
updateHandler.showDialog(getActivity());
updateHandler.showDialog();
}
}

View File

@ -0,0 +1,79 @@
package org.fdroid.fdroid.views.swap;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.support.v4.view.MenuItemCompat;
import android.util.AttributeSet;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.RelativeLayout;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.localrepo.SwapManager;
public class ConfirmReceive extends RelativeLayout implements SwapWorkflowActivity.InnerView {
public ConfirmReceive(Context context) {
super(context);
}
public ConfirmReceive(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ConfirmReceive(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ConfirmReceive(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private SwapWorkflowActivity getActivity() {
return (SwapWorkflowActivity)getContext();
}
private SwapManager getManager() {
return getActivity().getState();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
}
@Override
public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) {
return true;
}
@Override
public int getStep() {
return SwapManager.STEP_CONFIRM_SWAP;
}
@Override
public int getPreviousStep() {
return SwapManager.STEP_INTRO;
}
@ColorRes
public int getToolbarColour() {
return getResources().getColor(R.color.swap_blue);
}
@Override
public String getToolbarTitle() {
return getResources().getString(R.string.swap_confirm);
}
}

View File

@ -12,6 +12,7 @@ import android.os.Build;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
@ -28,17 +29,20 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.Switch;
import android.widget.TextView;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.localrepo.SwapManager;
import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.localrepo.peers.Peer;
import org.fdroid.fdroid.net.WifiStateChangeService;
import java.util.ArrayList;
public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.InnerView {
public class StartSwapView extends ScrollView implements SwapWorkflowActivity.InnerView {
private static final String TAG = "StartSwapView";
@ -111,12 +115,23 @@ public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.
protected void onFinishInflate() {
super.onFinishInflate();
getManager().scanForPeers();
uiInitPeers();
uiInitBluetooth();
uiInitWifi();
uiInitButtons();
uiUpdatePeersInfo();
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
uiUpdateWifi();
}
},
new IntentFilter(WifiStateChangeService.BROADCAST)
);
}
private void uiInitButtons() {
@ -161,9 +176,13 @@ public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.
@Override
public void onReceive(Context context, Intent intent) {
Peer peer = intent.getParcelableExtra(SwapManager.EXTRA_PEER);
Log.d(TAG, "Found peer: " + peer + ", adding to list of peers in UI.");
adapter.add(peer);
uiUpdatePeersInfo();
if (adapter.getPosition(peer) >= 0) {
Log.d(TAG, "Found peer: " + peer + ", ignoring though, because it is already in our list.");
} else {
Log.d(TAG, "Found peer: " + peer + ", adding to list of peers in UI.");
adapter.add(peer);
uiUpdatePeersInfo();
}
}
}, new IntentFilter(SwapManager.ACTION_PEER_FOUND));
@ -191,29 +210,51 @@ public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.
viewBluetoothId = (TextView)findViewById(R.id.device_id_bluetooth);
viewBluetoothId.setText(bluetooth.getName());
viewBluetoothId.setVisibility(bluetooth.isEnabled() ? View.VISIBLE : View.GONE);
int textResource = getManager().isBluetoothDiscoverable() ? R.string.swap_visible_bluetooth : R.string.swap_not_visible_bluetooth;
textBluetoothVisible.setText(textResource);
Switch bluetoothSwitch = ((Switch) findViewById(R.id.switch_bluetooth));
final Switch bluetoothSwitch = ((Switch) findViewById(R.id.switch_bluetooth));
bluetoothSwitch.setChecked(getManager().isBluetoothDiscoverable());
bluetoothSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
getManager().ensureBluetoothDiscoverable();
getManager().scanForPeers();
textBluetoothVisible.setText(R.string.swap_visible_bluetooth);
viewBluetoothId.setVisibility(View.VISIBLE);
uiUpdatePeersInfo();
// TODO: When they deny the request for enabling bluetooth, we need to disable this switch...
} else {
getManager().cancelScanningForPeers();
getManager().makeBluetoothNonDiscoverable();
textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth);
viewBluetoothId.setVisibility(View.GONE);
uiUpdatePeersInfo();
}
}
});
LocalBroadcastManager.getInstance(getContext()).registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.hasExtra(SwapService.EXTRA_STARTING)) {
Log.d(TAG, "Bluetooth service is starting...");
bluetoothSwitch.setEnabled(false);
bluetoothSwitch.setChecked(true);
} else {
bluetoothSwitch.setEnabled(true);
if (intent.hasExtra(SwapService.EXTRA_STARTED)) {
Log.d(TAG, "Bluetooth service has started.");
bluetoothSwitch.setChecked(true);
} else {
Log.d(TAG, "Bluetooth service has stopped.");
bluetoothSwitch.setChecked(false);
}
}
}
}, new IntentFilter(SwapService.BLUETOOTH_STATE_CHANGE));
} else {
findViewById(R.id.bluetooth_info).setVisibility(View.GONE);
}
@ -221,34 +262,56 @@ public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.
private void uiInitWifi() {
final TextView textWifiVisible = (TextView)findViewById(R.id.wifi_visible);
viewWifiId = (TextView)findViewById(R.id.device_id_wifi);
viewWifiNetwork = (TextView)findViewById(R.id.wifi_network);
int textResource = getManager().isBonjourDiscoverable() ? R.string.swap_visible_wifi : R.string.swap_not_visible_wifi;
textWifiVisible.setText(textResource);
Switch wifiSwitch = (Switch)findViewById(R.id.switch_wifi);
final Switch wifiSwitch = (Switch)findViewById(R.id.switch_wifi);
wifiSwitch.setChecked(getManager().isBonjourDiscoverable());
wifiSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
textWifiVisible.setText(R.string.swap_visible_wifi);
uiUpdatePeersInfo();
getManager().ensureBonjourDiscoverable();
} else {
textWifiVisible.setText(R.string.swap_not_visible_wifi);
uiUpdatePeersInfo();
getManager().makeBonjourNotDiscoverable();
}
uiUpdatePeersInfo();
uiUpdateWifi();
}
});
LocalBroadcastManager.getInstance(getContext()).registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.hasExtra(SwapService.EXTRA_STARTING)) {
Log.d(TAG, "Bonjour service is starting...");
wifiSwitch.setEnabled(false);
wifiSwitch.setChecked(true);
} else {
wifiSwitch.setEnabled(true);
if (intent.hasExtra(SwapService.EXTRA_STARTED)) {
Log.d(TAG, "Bonjour service has started.");
wifiSwitch.setChecked(true);
} else {
Log.d(TAG, "Bonjour service has stopped.");
wifiSwitch.setChecked(false);
}
}
uiUpdateWifi();
}
}, new IntentFilter(SwapService.BONJOUR_STATE_CHANGE));
uiUpdateWifi();
}
private void uiUpdateWifi() {
final TextView textWifiVisible = (TextView)findViewById(R.id.wifi_visible);
int textResource = getManager().isBonjourDiscoverable() ? R.string.swap_visible_wifi : R.string.swap_not_visible_wifi;
textWifiVisible.setText(textResource);
viewWifiId.setText(FDroidApp.ipAddressString);
viewWifiId.setVisibility(TextUtils.isEmpty(FDroidApp.ipAddressString) ? View.GONE : View.VISIBLE);
if (TextUtils.isEmpty(FDroidApp.bssid) && !TextUtils.isEmpty(FDroidApp.ipAddressString)) {
// empty bssid with an ipAddress means hotspot mode

View File

@ -0,0 +1,327 @@
package org.fdroid.fdroid.views.swap;
import android.annotation.TargetApi;
import android.content.Context;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Looper;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
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.support.v4.widget.CursorAdapter;
import android.support.v7.widget.SearchView;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.localrepo.SwapManager;
import java.util.Timer;
import java.util.TimerTask;
public class SwapAppsView extends ListView implements
SwapWorkflowActivity.InnerView,
LoaderManager.LoaderCallbacks<Cursor>,
SearchView.OnQueryTextListener {
public SwapAppsView(Context context) {
super(context);
}
public SwapAppsView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SwapAppsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public SwapAppsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private SwapWorkflowActivity getActivity() {
return (SwapWorkflowActivity)getContext();
}
private SwapManager getState() {
return getActivity().getState();
}
private static final int LOADER_SWAPABLE_APPS = 759283741;
private static final String TAG = "SwapAppsView";
private Repo repo;
private AppListAdapter adapter;
private String mCurrentFilterString;
@Override
protected void onFinishInflate() {
super.onFinishInflate();
repo = getActivity().getState().getPeerRepo();
if (repo == null) {
// TODO: Uh oh, something stuffed up for this to happen.
// TODO: What is the best course of action from here?
}
adapter = new AppListAdapter(getContext(), getContext().getContentResolver().query(
AppProvider.getRepoUri(repo), AppProvider.DataColumns.ALL, null, null, null));
setAdapter(adapter);
// either reconnect with an existing loader or start a new one
getActivity().getSupportLoaderManager().initLoader(LOADER_SWAPABLE_APPS, null, this);
setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
showAppDetails(position);
}
});
schedulePollForUpdates();
}
private void pollForUpdates() {
if (adapter.getCount() > 1 ||
(adapter.getCount() == 1 && !new App((Cursor)adapter.getItem(0)).id.equals("org.fdroid.fdroid"))) {
Log.d(TAG, "Not polling for new apps from swap repo, because we already have more than one.");
return;
}
Log.d(TAG, "Polling swap repo to see if it has any updates.");
UpdateService.UpdateReceiver receiver = getState().refreshSwap();
if (receiver != null) {
receiver.setListener(new ProgressListener() {
@Override
public void onProgress(Event event) {
switch (event.type) {
case UpdateService.EVENT_COMPLETE_WITH_CHANGES:
Log.d(TAG, "Swap repo has updates, notifying the list adapter.");
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
adapter.notifyDataSetChanged();
}
});
break;
case UpdateService.EVENT_ERROR:
// TODO: Well, if we can't get the index, we probably can't swapp apps.
// Tell the user somethign helpful?
break;
case UpdateService.EVENT_COMPLETE_AND_SAME:
schedulePollForUpdates();
break;
}
}
});
}
}
private void schedulePollForUpdates() {
Log.d(TAG, "Scheduling poll for updated swap repo in 5 seconds.");
new Timer().schedule(new TimerTask() {
@Override
public void run() {
Looper.prepare();
pollForUpdates();
Looper.loop();
}
}, 5000);
}
@Override
public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.swap_search, menu);
SearchView searchView = new SearchView(getActivity());
MenuItem searchMenuItem = menu.findItem(R.id.action_search);
MenuItemCompat.setActionView(searchMenuItem, searchView);
MenuItemCompat.setShowAsAction(searchMenuItem, MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
searchView.setOnQueryTextListener(this);
return true;
}
@Override
public int getStep() {
return SwapManager.STEP_SUCCESS;
}
@Override
public int getPreviousStep() {
return SwapManager.STEP_INTRO;
}
@ColorRes
public int getToolbarColour() {
return getResources().getColor(R.color.swap_bright_blue);
}
@Override
public String getToolbarTitle() {
return getResources().getString(R.string.swap_success);
}
private void showAppDetails(int position) {
Cursor c = (Cursor) adapter.getItem(position);
App app = new App(c);
// TODO: Show app details screen.
}
@Override
public CursorLoader onCreateLoader(int id, Bundle args) {
Uri uri = TextUtils.isEmpty(mCurrentFilterString)
? AppProvider.getRepoUri(repo)
: AppProvider.getSearchUri(repo, mCurrentFilterString);
return new CursorLoader(getActivity(), uri, AppProvider.DataColumns.ALL, null, null, AppProvider.DataColumns.NAME);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
adapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.swapCursor(null);
}
@Override
public boolean onQueryTextChange(String newText) {
String newFilter = !TextUtils.isEmpty(newText) ? newText : null;
if (mCurrentFilterString == null && newFilter == null) {
return true;
}
if (mCurrentFilterString != null && mCurrentFilterString.equals(newFilter)) {
return true;
}
mCurrentFilterString = newFilter;
getActivity().getSupportLoaderManager().restartLoader(LOADER_SWAPABLE_APPS, null, this);
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
// this is not needed since we respond to every change in text
return true;
}
private class AppListAdapter extends CursorAdapter {
@SuppressWarnings("UnusedDeclaration")
private static final String TAG = "AppListAdapter";
@Nullable
private LayoutInflater inflater;
@Nullable
private Drawable defaultAppIcon;
public AppListAdapter(@NonNull Context context, @Nullable Cursor c) {
super(context, c, FLAG_REGISTER_CONTENT_OBSERVER);
}
@NonNull
private LayoutInflater getInflater(Context context) {
if (inflater == null) {
inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
return inflater;
}
private Drawable getDefaultAppIcon(Context context) {
if (defaultAppIcon == null) {
defaultAppIcon = context.getResources().getDrawable(android.R.drawable.sym_def_app_icon);
}
return defaultAppIcon;
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View view = getInflater(context).inflate(R.layout.swap_app_list_item, parent, false);
bindView(view, context, cursor);
return view;
}
@Override
public void bindView(final View view, final Context context, final Cursor cursor) {
TextView nameView = (TextView)view.findViewById(R.id.name);
ImageView iconView = (ImageView)view.findViewById(android.R.id.icon);
Button button = (Button)view.findViewById(R.id.button);
TextView status = (TextView)view.findViewById(R.id.status);
final App app = new App(cursor);
nameView.setText(app.name);
iconView.setImageDrawable(getDefaultAppIcon(context)); // TODO: Load icon from repo properly using UIL.
if (app.hasUpdates()) {
button.setText(R.string.menu_upgrade);
button.setEnabled(true);
button.setBackgroundColor(getResources().getColor(R.color.fdroid_blue));
button.setVisibility(View.VISIBLE);
status.setVisibility(View.GONE);
} else if (app.isInstalled()) {
status.setText(R.string.inst);
status.setTextColor(getResources().getColor(R.color.fdroid_green));
status.setVisibility(View.VISIBLE);
button.setVisibility(View.GONE);
} else if (!app.compatible) {
status.setText(R.string.incompatible);
status.setTextColor(getResources().getColor(R.color.swap_light_grey_icon));
status.setVisibility(View.VISIBLE);
button.setVisibility(View.GONE);
} else {
button.setText(R.string.menu_install);
button.setEnabled(true);
button.setBackgroundColor(getResources().getColor(R.color.fdroid_green));
button.setVisibility(View.VISIBLE);
status.setVisibility(View.GONE);
}
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (app.hasUpdates() || app.compatible) {
getState().install(app);
}
}
});
}
}
}

View File

@ -16,6 +16,7 @@ import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.ProgressListener;
@ -24,6 +25,8 @@ import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.localrepo.SwapManager;
import org.fdroid.fdroid.localrepo.peers.Peer;
import java.util.List;
public class SwapConnecting extends LinearLayout implements SwapWorkflowActivity.InnerView {
private final static String TAG = "SwapConnecting";
@ -77,6 +80,31 @@ public class SwapConnecting extends LinearLayout implements SwapWorkflowActivity
@Override
public void onProgress(Event event) {
((TextView) findViewById(R.id.progress)).setText(event.data.getString(UpdateService.EXTRA_ADDRESS));
boolean finished = false;
boolean error = false;
switch (event.type) {
case UpdateService.EVENT_ERROR:
finished = true;
error = true;
break;
case UpdateService.EVENT_COMPLETE_WITH_CHANGES:
finished = true;
break;
case UpdateService.EVENT_COMPLETE_AND_SAME:
finished = true;
break;
case UpdateService.EVENT_INFO:
break;
}
if (finished) {
if (error) {
// TODO: Feedback to user about error, suggest fixes.
} else {
getActivity().showSwapConnected();
}
}
}
});
}

View File

@ -237,6 +237,10 @@ public class SwapWorkflowActivity extends AppCompatActivity {
inflateInnerView(R.layout.swap_wifi_qr);
}
public void showSwapConnected() {
inflateInnerView(R.layout.swap_success);
}
private boolean attemptToShowNfc() {
// TODO: What if NFC is disabled? Hook up with NfcNotEnabledActivity? Or maybe only if they
// click a relevant button?
@ -286,9 +290,9 @@ public class SwapWorkflowActivity extends AppCompatActivity {
}
@Override
protected void onPostExecute(Void v) {
super.onPostExecute(v);
protected void onPreExecute() {
state.enableSwapping();
super.onPreExecute();
}
}