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. +