From ab88ca252bbe681d1fb3f2b588be2b2537118ccf Mon Sep 17 00:00:00 2001 From: Jessie Chatham Spencer Date: Thu, 6 Jul 2017 07:55:18 +0200 Subject: [PATCH] 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. --- .../activities/MainActivity.java | 54 ++++++++++++++++++ .../fragments/DrawerFragment.java | 46 +++++++++------ .../syncthingandroid/http/ApiRequest.java | 36 ++++++++++++ .../http/ImageGetRequest.java | 29 ++++++++++ .../syncthingandroid/service/RestApi.java | 4 ++ src/main/res/layout/dialog_qrcode.xml | 56 +++++++++++++++++++ src/main/res/layout/fragment_drawer.xml | 51 ++++------------- src/main/res/values/strings.xml | 5 ++ 8 files changed, 225 insertions(+), 56 deletions(-) create mode 100644 src/main/java/com/nutomic/syncthingandroid/http/ImageGetRequest.java create mode 100644 src/main/res/layout/dialog_qrcode.xml diff --git a/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java b/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java index b7fd0714..bc7b1ebf 100644 --- a/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java +++ b/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java @@ -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); diff --git a/src/main/java/com/nutomic/syncthingandroid/fragments/DrawerFragment.java b/src/main/java/com/nutomic/syncthingandroid/fragments/DrawerFragment.java index 9029ed52..fb95a200 100644 --- a/src/main/java/com/nutomic/syncthingandroid/fragments/DrawerFragment.java +++ b/src/main/java/com/nutomic/syncthingandroid/fragments/DrawerFragment.java @@ -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; } } } diff --git a/src/main/java/com/nutomic/syncthingandroid/http/ApiRequest.java b/src/main/java/com/nutomic/syncthingandroid/http/ApiRequest.java index 6285e972..4c674c9d 100644 --- a/src/main/java/com/nutomic/syncthingandroid/http/ApiRequest.java +++ b/src/main/java/com/nutomic/syncthingandroid/http/ApiRequest.java @@ -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 getHeaders() throws AuthFailureError { + return ImmutableMap.of(HEADER_API_KEY, mApiKey); + } + }; + + getVolleyQueue().add(imageRequest); + } + /** * Extends {@link HurlStack}, uses {@link #getSslSocketFactory()} and disables hostname * verification. diff --git a/src/main/java/com/nutomic/syncthingandroid/http/ImageGetRequest.java b/src/main/java/com/nutomic/syncthingandroid/http/ImageGetRequest.java new file mode 100644 index 00000000..10d82535 --- /dev/null +++ b/src/main/java/com/nutomic/syncthingandroid/http/ImageGetRequest.java @@ -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 params, OnImageSuccessListener onSuccessListener, OnErrorListener onErrorListener) { + super(context, url, path, httpsCertPath, apiKey); + Map safeParams = Optional.fromNullable(params).or(Collections.emptyMap()); + Uri uri = buildUri(safeParams); + makeImageRequest(uri, onSuccessListener, onErrorListener); + } +} diff --git a/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java b/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java index b4141901..0583d7b4 100644 --- a/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java +++ b/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java @@ -501,4 +501,8 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, public void setRestartPostponed() { mRestartPostponed = true; } + + public URL getUrl() { + return mUrl; + } } diff --git a/src/main/res/layout/dialog_qrcode.xml b/src/main/res/layout/dialog_qrcode.xml new file mode 100644 index 00000000..a0b5aec5 --- /dev/null +++ b/src/main/res/layout/dialog_qrcode.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/fragment_drawer.xml b/src/main/res/layout/fragment_drawer.xml index e6ade5d8..6b58d8df 100644 --- a/src/main/res/layout/fragment_drawer.xml +++ b/src/main/res/layout/fragment_drawer.xml @@ -53,35 +53,6 @@ android:layout_height="wrap_content" android:orientation="vertical"> - - - - - - + + - - Maximum Age: %1$d \nVersions Path: %2$s Command: %1$s Keep versions: %1$s + Show device ID + + + Could not access device ID. +