Added dialog displaying device ID and QRCode addressing #790 (#912)

* Wip adding dialog.

* Wip adding ImageRequest functionality.

* Got a text converted to a QrCode using the syncthing Api.

* Dialog now shows device ID and QRCode.
The dialog also keeps it state when the device rotates and the copy ID functionality has been moved
to the dialog, from the drawer.

* Moved share device ID functionality to the Device ID dialog.

* Addressed code change requests and added improved dialog layout by @nutomic.
This commit is contained in:
Jessie Chatham Spencer 2017-07-06 07:55:18 +02:00 committed by Felix Ableitner
parent 825a62b902
commit ab88ca252b
8 changed files with 225 additions and 56 deletions

View File

@ -12,6 +12,11 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.Image;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@ -36,7 +41,9 @@ import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.fragments.DeviceListFragment;
@ -44,7 +51,13 @@ import com.nutomic.syncthingandroid.fragments.DrawerFragment;
import com.nutomic.syncthingandroid.fragments.FolderListFragment;
import com.nutomic.syncthingandroid.model.Options;
import com.nutomic.syncthingandroid.service.SyncthingService;
import com.nutomic.syncthingandroid.util.Util;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@ -61,6 +74,9 @@ public class MainActivity extends SyncthingActivity
private static final String TAG = "MainActivity";
private static final String IS_SHOWING_RESTART_DIALOG = "RESTART_DIALOG_STATE";
private static final String BATTERY_DIALOG_DISMISSED = "BATTERY_DIALOG_STATE";
private static final String IS_QRCODE_DIALOG_DISPLAYED = "QRCODE_DIALOG_STATE";
private static final String QRCODE_BITMAP_KEY = "QRCODE_BITMAP";
private static final String DEVICEID_KEY = "DEVICEID";
/**
* Time after first start when usage reporting dialog should be shown.
@ -71,6 +87,7 @@ public class MainActivity extends SyncthingActivity
private AlertDialog mDisabledDialog;
private AlertDialog mBatteryOptimizationsDialog;
private AlertDialog mQrCodeDialog;
private Dialog mRestartDialog;
private boolean mBatteryOptimizationDialogDismissed;
@ -224,6 +241,9 @@ public class MainActivity extends SyncthingActivity
showRestartDialog();
}
mBatteryOptimizationDialogDismissed = savedInstanceState.getBoolean(BATTERY_DIALOG_DISMISSED);
if(savedInstanceState.getBoolean(IS_QRCODE_DIALOG_DISPLAYED)) {
showQrCodeDialog(savedInstanceState.getString(DEVICEID_KEY), savedInstanceState.getParcelable(QRCODE_BITMAP_KEY));
}
} else {
mFolderListFragment = new FolderListFragment();
mDeviceListFragment = new DeviceListFragment();
@ -276,6 +296,13 @@ public class MainActivity extends SyncthingActivity
outState.putInt("currentTab", mViewPager.getCurrentItem());
outState.putBoolean(BATTERY_DIALOG_DISMISSED, mBatteryOptimizationsDialog == null || !mBatteryOptimizationsDialog.isShowing());
outState.putBoolean(IS_SHOWING_RESTART_DIALOG, mRestartDialog != null && mRestartDialog.isShowing());
if(mQrCodeDialog != null && mQrCodeDialog.isShowing()) {
outState.putBoolean(IS_QRCODE_DIALOG_DISPLAYED, true);
ImageView qrCode = (ImageView) mQrCodeDialog.findViewById(R.id.qrcode_image_view);
TextView deviceID = (TextView) mQrCodeDialog.findViewById(R.id.device_id);
outState.putParcelable(QRCODE_BITMAP_KEY, ((BitmapDrawable) qrCode.getDrawable()).getBitmap());
outState.putString(DEVICEID_KEY, deviceID.getText().toString());
}
if (mRestartDialog != null){
mRestartDialog.cancel();
}
@ -332,6 +359,33 @@ public class MainActivity extends SyncthingActivity
.create();
}
public void showQrCodeDialog(String deviceId, Bitmap qrCode) {
View qrCodeDialogView = this.getLayoutInflater().inflate(R.layout.dialog_qrcode, null);
TextView deviceIdTextView = (TextView) qrCodeDialogView.findViewById(R.id.device_id);
TextView shareDeviceIdTextView = (TextView) qrCodeDialogView.findViewById(R.id.actionShareId);
ImageView qrCodeImageView = (ImageView) qrCodeDialogView.findViewById(R.id.qrcode_image_view);
deviceIdTextView.setText(deviceId);
deviceIdTextView.setOnClickListener(v -> Util.copyDeviceId(this, deviceIdTextView.getText().toString()));
shareDeviceIdTextView.setOnClickListener(v -> shareDeviceId(deviceId));
qrCodeImageView.setImageBitmap(qrCode);
mQrCodeDialog = new AlertDialog.Builder(this)
.setTitle(R.string.device_id)
.setView(qrCodeDialogView)
.setPositiveButton(R.string.finish, null)
.create();
mQrCodeDialog.show();
}
private void shareDeviceId(String deviceId) {
Intent shareIntent = new Intent(android.content.Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(android.content.Intent.EXTRA_TEXT, deviceId);
startActivity(Intent.createChooser(shareIntent, "Share device ID with"));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return mDrawerToggle.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);

View File

@ -1,21 +1,23 @@
package com.nutomic.syncthingandroid.fragments;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v4.util.ArrayMap;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.activities.MainActivity;
import com.nutomic.syncthingandroid.activities.SettingsActivity;
import com.nutomic.syncthingandroid.activities.WebGuiActivity;
import com.nutomic.syncthingandroid.http.ImageGetRequest;
import com.nutomic.syncthingandroid.model.Connections;
import com.nutomic.syncthingandroid.model.SystemInfo;
import com.nutomic.syncthingandroid.model.SystemVersion;
@ -23,6 +25,8 @@ import com.nutomic.syncthingandroid.service.RestApi;
import com.nutomic.syncthingandroid.service.SyncthingService;
import com.nutomic.syncthingandroid.util.Util;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.Map;
@ -34,7 +38,6 @@ import java.util.TimerTask;
*/
public class DrawerFragment extends Fragment implements View.OnClickListener {
private TextView mDeviceId;
private TextView mCpuUsage;
private TextView mRamUsage;
private TextView mDownload;
@ -43,6 +46,8 @@ public class DrawerFragment extends Fragment implements View.OnClickListener {
private TextView mVersion;
private TextView mExitButton;
private String mDeviceId;
private Timer mTimer;
private MainActivity mActivity;
@ -87,7 +92,6 @@ public class DrawerFragment extends Fragment implements View.OnClickListener {
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
mDeviceId = (TextView) view.findViewById(R.id.device_id);
mCpuUsage = (TextView) view.findViewById(R.id.cpu_usage);
mRamUsage = (TextView) view.findViewById(R.id.ram_usage);
mDownload = (TextView) view.findViewById(R.id.download);
@ -98,12 +102,12 @@ public class DrawerFragment extends Fragment implements View.OnClickListener {
view.findViewById(R.id.drawerActionWebGui)
.setOnClickListener(this);
view.findViewById(R.id.drawerActionShareId)
.setOnClickListener(this);
view.findViewById(R.id.drawerActionRestart)
.setOnClickListener(this);
view.findViewById(R.id.drawerActionSettings)
.setOnClickListener(this);
view.findViewById(R.id.drawerActionShowQrCode)
.setOnClickListener(this);
mExitButton.setOnClickListener(this);
updateExitButtonVisibility();
@ -156,9 +160,7 @@ public class DrawerFragment extends Fragment implements View.OnClickListener {
public void onReceiveSystemInfo(SystemInfo info) {
if (getActivity() == null)
return;
mDeviceId.setText(info.myID);
mDeviceId.setOnClickListener(v -> Util.copyDeviceId(getActivity(), mDeviceId.getText().toString()));
mDeviceId = info.myID;
NumberFormat percentFormat = NumberFormat.getPercentInstance();
percentFormat.setMaximumFractionDigits(2);
mCpuUsage.setText(percentFormat.format(info.cpuPercent / 100));
@ -193,6 +195,22 @@ public class DrawerFragment extends Fragment implements View.OnClickListener {
mUpload.setText(Util.readableTransferRate(mActivity, c.outBits));
}
/**
* Gets QRCode and displays it in a Dialog.
*/
private void showQrCode() {
//The QRCode request takes one paramteer called "text", which is the text to be converted to a QRCode.
String httpsCertPath = mActivity.getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE;
String apiKey = mActivity.getApi().getGui().apiKey;
URL url = mActivity.getApi().getUrl();
new ImageGetRequest(mActivity, url, ImageGetRequest.QR_CODE_GENERATOR, httpsCertPath,
apiKey, ImmutableMap.of("text", mDeviceId),qrCodeBitmap -> {
mActivity.showQrCodeDialog(mDeviceId, qrCodeBitmap);
mActivity.closeDrawer();
}, error -> Toast.makeText(mActivity, R.string.could_not_access_deviceid, Toast.LENGTH_SHORT).show());
}
@Override
public void onClick(View v) {
switch (v.getId()) {
@ -200,13 +218,6 @@ public class DrawerFragment extends Fragment implements View.OnClickListener {
startActivity(new Intent(mActivity, WebGuiActivity.class));
mActivity.closeDrawer();
break;
case R.id.drawerActionShareId:
Intent i = new Intent(android.content.Intent.ACTION_SEND);
i.setType("text/plain");
i.putExtra(android.content.Intent.EXTRA_TEXT, mDeviceId.getText());
startActivity(Intent.createChooser(i, "Share device ID with"));
mActivity.closeDrawer();
break;
case R.id.drawerActionSettings:
startActivity(new Intent(mActivity, SettingsActivity.class));
mActivity.closeDrawer();
@ -220,6 +231,9 @@ public class DrawerFragment extends Fragment implements View.OnClickListener {
mActivity.finish();
mActivity.closeDrawer();
break;
case R.id.drawerActionShowQrCode:
showQrCode();
break;
}
}
}

View File

@ -3,19 +3,27 @@ package com.nutomic.syncthingandroid.http;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.util.Log;
import android.widget.ImageView;
import com.android.volley.AuthFailureError;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HurlStack;
import com.android.volley.toolbox.ImageRequest;
import com.android.volley.toolbox.JsonObjectRequest;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import org.json.JSONObject;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
@ -54,6 +62,10 @@ public abstract class ApiRequest {
public void onSuccess(String result);
}
public interface OnImageSuccessListener {
public void onImageSuccess(Bitmap result);
}
public interface OnErrorListener {
public void onError(VolleyError error);
}
@ -119,6 +131,30 @@ public abstract class ApiRequest {
getVolleyQueue().add(request);
}
/**
* Opens the connection, then returns success status and response bitmap.
*/
protected void makeImageRequest(Uri uri, @Nullable OnImageSuccessListener imageListener,
@Nullable OnErrorListener errorListener) {
ImageRequest imageRequest = new ImageRequest(uri.toString(), bitmap -> {
if (imageListener != null) {
imageListener.onImageSuccess(bitmap);
}
}, 0, 0, ImageView.ScaleType.CENTER, Bitmap.Config.RGB_565, volleyError -> {
if(errorListener != null) {
errorListener.onError(volleyError);
}
Log.d(TAG, "onErrorResponse: " + volleyError);
}) {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
return ImmutableMap.of(HEADER_API_KEY, mApiKey);
}
};
getVolleyQueue().add(imageRequest);
}
/**
* Extends {@link HurlStack}, uses {@link #getSslSocketFactory()} and disables hostname
* verification.

View File

@ -0,0 +1,29 @@
package com.nutomic.syncthingandroid.http;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.android.volley.Request;
import com.google.common.base.Optional;
import java.net.URL;
import java.util.Collections;
import java.util.Map;
/**
* Created by jmintb on 27-06-17.
*/
public class ImageGetRequest extends ApiRequest {
public static String QR_CODE_GENERATOR = "/qr/";
public ImageGetRequest(Context context, URL url, String path, String httpsCertPath, String apiKey,
@Nullable Map<String, String> params, OnImageSuccessListener onSuccessListener, OnErrorListener onErrorListener) {
super(context, url, path, httpsCertPath, apiKey);
Map<String, String> safeParams = Optional.fromNullable(params).or(Collections.emptyMap());
Uri uri = buildUri(safeParams);
makeImageRequest(uri, onSuccessListener, onErrorListener);
}
}

View File

@ -501,4 +501,8 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
public void setRestartPostponed() {
mRestartPostponed = true;
}
public URL getUrl() {
return mUrl;
}
}

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/device_id"
style="@style/Widget.Syncthing.TextView.Label"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1"
android:padding="8dp"
android:clickable="true"
android:drawableEnd="@drawable/ic_content_copy_black_24dp"
android:drawableRight="@drawable/ic_content_copy_black_24dp"
android:focusable="true"
android:fontFamily="monospace"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
tools:text="ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD"/>
<TextView
android:id="@+id/actionShareId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="8dp"
android:drawableRight="@drawable/ic_share_black_24dp"
android:drawableEnd="@drawable/ic_share_black_24dp"
android:clickable="true"
android:focusable="true" />
</LinearLayout>
<ImageView
android:id="@+id/qrcode_image_view"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_horizontal"
tools:src="@drawable/ic_launcher"/>
</LinearLayout>
</ScrollView>

View File

@ -53,35 +53,6 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
style="@style/Widget.Syncthing.TextView.Label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="vertical"
android:paddingBottom="6dp"
android:paddingTop="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/device_id"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
<TextView
style="@style/Widget.Syncthing.TextView.Label"
android:id="@+id/device_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:drawableRight="@drawable/ic_content_copy_black_24dp"
android:drawableEnd="@drawable/ic_content_copy_black_24dp"
android:clickable="true"
android:focusable="true" />
</LinearLayout>
<View
android:layout_width="match_parent"
@ -238,6 +209,17 @@
android:layout_marginBottom="4dp"
android:background="@drawable/list_divider" />
<TextView
android:id="@+id/drawerActionShowQrCode"
style="@style/Widget.Syncthing.TextView.Label"
android:layout_width="match_parent"
android:layout_height="48dp"
android:clickable="true"
android:drawableLeft="@drawable/ic_qrcode_black_24dp_active"
android:drawableStart="@drawable/ic_qrcode_black_24dp_active"
android:focusable="true"
android:text="@string/show_device_id" />
<TextView
android:id="@+id/drawerActionWebGui"
style="@style/Widget.Syncthing.TextView.Label"
@ -249,17 +231,6 @@
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/drawerActionShareId"
style="@style/Widget.Syncthing.TextView.Label"
android:layout_width="match_parent"
android:layout_height="48dp"
android:drawableLeft="@drawable/ic_share_black_24dp"
android:drawableStart="@drawable/ic_share_black_24dp"
android:text="@string/share_device_id"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/drawerActionRestart"
style="@style/Widget.Syncthing.TextView.Label"

View File

@ -595,4 +595,9 @@ Please report any problems you encounter via Github.</string>
<string name="staggered_versioning_info">Maximum Age: %1$d \nVersions Path: %2$s</string>
<string name="external_versioning_info">Command: %1$s</string>
<string name="simple_versioning_info">Keep versions: %1$s</string>
<string name="show_device_id">Show device ID</string>
<!-- error message if the deviceID/QRCode dialog for some reason cannot be displayed.-->
<string name="could_not_access_deviceid">Could not access device ID.</string>
</resources>