diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 7566503dda..6dfcbb52d5 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -131,12 +131,6 @@ - - - - - - - diff --git a/build.gradle b/build.gradle index deb4bc1193..bd43b598f6 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,7 @@ dependencies { compile 'com.android.support:appcompat-v7:22.2.1' compile 'com.android.support:recyclerview-v7:22.2.1' compile 'com.android.support:design:22.2.1' + compile 'com.android.support:cardview-v7:22.2.1' compile 'com.melnykov:floatingactionbutton:1.3.0' compile 'com.google.zxing:android-integration:3.1.0' compile ('com.android.support:support-v4-preferencefragment:1.0.0@aar'){ @@ -71,6 +72,7 @@ dependencies { compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' compile 'org.whispersystems:textsecure-android:1.8.3' compile 'com.h6ah4i.android.compat:mulsellistprefcompat:1.0.0' + compile 'com.google.zxing:core:3.2.1' testCompile 'junit:junit:4.12' testCompile 'org.assertj:assertj-core:1.7.1' @@ -109,6 +111,7 @@ dependencyVerification { 'com.android.support:appcompat-v7:4b5ccba8c4557ef04f99aa0a80f8aa7d50f05f926a709010a54afd5c878d3618', 'com.android.support:recyclerview-v7:b0f530a5b14334d56ce0de85527ffe93ac419bc928e2884287ce1dddfedfb505', 'com.android.support:design:58be3ca6a73789615f7ece0937d2f683b98b594bb90aa10565fa760fb10b07ee', + 'com.android.support:cardview-v7:2c2354761a4e20ba451ae903ab808f15c9acc8343b1e74001869c2d0a672c1fc', 'com.melnykov:floatingactionbutton:15d58d4fac0f7a288d0e5301bbaf501a146f5b3f5921277811bf99bd3b397263', 'com.google.zxing:android-integration:89e56aadf1164bd71e57949163c53abf90af368b51669c0d4a47a163335f95c4', 'com.android.support:support-v4-preferencefragment:5470f5872514a6226fa1fc6f4e000991f38805691c534cf0bd2778911fc773ad', @@ -121,6 +124,7 @@ dependencyVerification { 'com.amulyakhare:com.amulyakhare.textdrawable:54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb', 'org.whispersystems:textsecure-android:a08cdd73aaaca6d3e868a93522c02d6a159551735f7048b1f3a53582e10c8ec2', 'com.h6ah4i.android.compat:mulsellistprefcompat:47167c5cb796de1a854788e9ff318358e36c8fb88123baaa6e38fb78511dfabe', + 'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259', 'com.android.support:support-annotations:104f353b53d5dd8d64b2f77eece4b37f6b961de9732eb6b706395e91033ec70a', 'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a', 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', diff --git a/res/drawable-hdpi/ic_add_white_original_24dp.png b/res/drawable-hdpi/ic_add_white_original_24dp.png new file mode 100644 index 0000000000..481643ecd5 Binary files /dev/null and b/res/drawable-hdpi/ic_add_white_original_24dp.png differ diff --git a/res/drawable-hdpi/ic_devices_white.png b/res/drawable-hdpi/ic_devices_white.png new file mode 100644 index 0000000000..38d1c0c046 Binary files /dev/null and b/res/drawable-hdpi/ic_devices_white.png differ diff --git a/res/drawable-hdpi/ic_laptop_black_32dp.png b/res/drawable-hdpi/ic_laptop_black_32dp.png new file mode 100644 index 0000000000..c87e01d547 Binary files /dev/null and b/res/drawable-hdpi/ic_laptop_black_32dp.png differ diff --git a/res/drawable-hdpi/ic_laptop_light_32dp.png b/res/drawable-hdpi/ic_laptop_light_32dp.png new file mode 100644 index 0000000000..0f8d1680fb Binary files /dev/null and b/res/drawable-hdpi/ic_laptop_light_32dp.png differ diff --git a/res/drawable-mdpi/ic_add_white_original_24dp.png b/res/drawable-mdpi/ic_add_white_original_24dp.png new file mode 100644 index 0000000000..977dd3427a Binary files /dev/null and b/res/drawable-mdpi/ic_add_white_original_24dp.png differ diff --git a/res/drawable-mdpi/ic_devices_white.png b/res/drawable-mdpi/ic_devices_white.png new file mode 100644 index 0000000000..32fad78388 Binary files /dev/null and b/res/drawable-mdpi/ic_devices_white.png differ diff --git a/res/drawable-mdpi/ic_laptop_black_32dp.png b/res/drawable-mdpi/ic_laptop_black_32dp.png new file mode 100644 index 0000000000..795f9991d3 Binary files /dev/null and b/res/drawable-mdpi/ic_laptop_black_32dp.png differ diff --git a/res/drawable-mdpi/ic_laptop_light_32dp.png b/res/drawable-mdpi/ic_laptop_light_32dp.png new file mode 100644 index 0000000000..58d056ffd6 Binary files /dev/null and b/res/drawable-mdpi/ic_laptop_light_32dp.png differ diff --git a/res/drawable-xhdpi/ic_add_white_original_24dp.png b/res/drawable-xhdpi/ic_add_white_original_24dp.png new file mode 100644 index 0000000000..67042105d2 Binary files /dev/null and b/res/drawable-xhdpi/ic_add_white_original_24dp.png differ diff --git a/res/drawable-xhdpi/ic_devices_white.png b/res/drawable-xhdpi/ic_devices_white.png new file mode 100644 index 0000000000..b77159e119 Binary files /dev/null and b/res/drawable-xhdpi/ic_devices_white.png differ diff --git a/res/drawable-xhdpi/ic_laptop_black_32dp.png b/res/drawable-xhdpi/ic_laptop_black_32dp.png new file mode 100644 index 0000000000..c1228164b2 Binary files /dev/null and b/res/drawable-xhdpi/ic_laptop_black_32dp.png differ diff --git a/res/drawable-xhdpi/ic_laptop_light_32dp.png b/res/drawable-xhdpi/ic_laptop_light_32dp.png new file mode 100644 index 0000000000..34e1a1efb4 Binary files /dev/null and b/res/drawable-xhdpi/ic_laptop_light_32dp.png differ diff --git a/res/drawable-xxhdpi/ic_add_white_original_24dp.png b/res/drawable-xxhdpi/ic_add_white_original_24dp.png new file mode 100644 index 0000000000..72cedcad4f Binary files /dev/null and b/res/drawable-xxhdpi/ic_add_white_original_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_devices_white.png b/res/drawable-xxhdpi/ic_devices_white.png new file mode 100644 index 0000000000..e2ebaaccef Binary files /dev/null and b/res/drawable-xxhdpi/ic_devices_white.png differ diff --git a/res/drawable-xxhdpi/ic_laptop_black_32dp.png b/res/drawable-xxhdpi/ic_laptop_black_32dp.png new file mode 100644 index 0000000000..c86e7c4ada Binary files /dev/null and b/res/drawable-xxhdpi/ic_laptop_black_32dp.png differ diff --git a/res/drawable-xxhdpi/ic_laptop_light_32dp.png b/res/drawable-xxhdpi/ic_laptop_light_32dp.png new file mode 100644 index 0000000000..f13d25e424 Binary files /dev/null and b/res/drawable-xxhdpi/ic_laptop_light_32dp.png differ diff --git a/res/drawable-xxxhdpi/ic_add_white_original_24dp.png b/res/drawable-xxxhdpi/ic_add_white_original_24dp.png new file mode 100644 index 0000000000..2bef059583 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_add_white_original_24dp.png differ diff --git a/res/drawable-xxxhdpi/ic_devices_white.png b/res/drawable-xxxhdpi/ic_devices_white.png new file mode 100644 index 0000000000..e3f2216f4d Binary files /dev/null and b/res/drawable-xxxhdpi/ic_devices_white.png differ diff --git a/res/layout/device_add_fragment.xml b/res/layout/device_add_fragment.xml new file mode 100644 index 0000000000..019e32cc2f --- /dev/null +++ b/res/layout/device_add_fragment.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/device_link_fragment.xml b/res/layout/device_link_fragment.xml new file mode 100644 index 0000000000..f67863d6fe --- /dev/null +++ b/res/layout/device_link_fragment.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/device_list_fragment.xml b/res/layout/device_list_fragment.xml index 4eb43f68e7..7b73e96b13 100644 --- a/res/layout/device_list_fragment.xml +++ b/res/layout/device_list_fragment.xml @@ -2,9 +2,9 @@ + xmlns:tools="http://schemas.android.com/tools" + xmlns:fab="http://schemas.android.com/apk/res-auto" + android:orientation="vertical"> + android:text="@string/device_list_fragment__no_devices_linked" + android:paddingLeft="16dip" + android:paddingRight="16dip" + android:layout_weight="1" + tools:visibility="visible"/> + android:drawSelectorOnTop="false" + android:paddingLeft="16dip" + android:paddingRight="16dip" + tools:visibility="gone"/> + \ No newline at end of file diff --git a/res/transition/fragment_shared.xml b/res/transition/fragment_shared.xml new file mode 100644 index 0000000000..42095a7762 --- /dev/null +++ b/res/transition/fragment_shared.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 68453ca171..433040b358 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -147,4 +147,13 @@ + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index e845da73ef..9890a17ed8 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -345,12 +345,12 @@ Link this device? It will be able to - - Read all your messages - \n- Send messages in your name + • Read all your messages + \n• Send messages in your name Linking device Linking new device... - Device linked! + Device approved! No device found. Network error. Invalid QR code. @@ -1128,6 +1128,9 @@ Transport icon + Scan the QR code displayed on the device to link + Link device + Link new device diff --git a/res/values/themes.xml b/res/values/themes.xml index 3d65e4a0d6..fe39dfaa7d 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -188,7 +188,7 @@ @drawable/ic_app_protection_black @drawable/ic_brightness_6_black @drawable/ic_forum_black_32dp - @drawable/ic_devices_black_48dp + @drawable/ic_laptop_black_32dp @drawable/ic_advanced_black @style/BetterPickersDialogFragment.Light @@ -301,7 +301,7 @@ @drawable/ic_app_protection_gray @drawable/ic_brightness_6_gray @drawable/ic_forum_grey_32dp - @drawable/ic_devices_grey600_48dp + @drawable/ic_laptop_light_32dp @drawable/ic_advanced_gray @style/BetterPickersDialogFragment diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 459324a05b..1fff7058fc 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -21,11 +21,9 @@ android:title="@string/preferences__chats" android:icon="?pref_ic_chats"/> - - - - - + = Build.VERSION_CODES.LOLLIPOP) { + deviceAddFragment.setSharedElementReturnTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared)); + deviceAddFragment.setExitTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade)); + + deviceLinkFragment.setSharedElementEnterTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared)); + deviceLinkFragment.setEnterTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade)); + + getSupportFragmentManager().beginTransaction() + .addToBackStack(null) + .addSharedElement(deviceAddFragment.getDevicesImage(), "devices") + .replace(android.R.id.content, deviceLinkFragment) + .commit(); + + } else { + getSupportFragmentManager().beginTransaction() + .setCustomAnimations(R.anim.slide_from_bottom, R.anim.slide_to_bottom, + R.anim.slide_from_bottom, R.anim.slide_to_bottom) + .replace(android.R.id.content, deviceLinkFragment) + .addToBackStack(null) + .commit(); + } + } + }); + } + + @Override + public void onLink(final Uri uri) { + new ProgressDialogAsyncTask(this, + R.string.DeviceProvisioningActivity_content_progress_title, + R.string.DeviceProvisioningActivity_content_progress_content) + { + private static final int SUCCESS = 0; + private static final int NO_DEVICE = 1; + private static final int NETWORK_ERROR = 2; + private static final int KEY_ERROR = 3; + private static final int LIMIT_EXCEEDED = 4; + + @Override + protected Integer doInBackground(Void... params) { + try { + Context context = DeviceActivity.this; + TextSecureAccountManager accountManager = TextSecureCommunicationFactory.createManager(context); + String verificationCode = accountManager.getNewDeviceVerificationCode(); + String ephemeralId = uri.getQueryParameter("uuid"); + String publicKeyEncoded = uri.getQueryParameter("pub_key"); + ECPublicKey publicKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); + IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context); + + accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, verificationCode); + TextSecurePreferences.setMultiDevice(context, true); + return SUCCESS; + } catch (NotFoundException e) { + Log.w(TAG, e); + return NO_DEVICE; + } catch (DeviceLimitExceededException e) { + Log.w(TAG, e); + return LIMIT_EXCEEDED; + } catch (IOException e) { + Log.w(TAG, e); + return NETWORK_ERROR; + } catch (InvalidKeyException e) { + Log.w(TAG, e); + return KEY_ERROR; + } + } + + @Override + protected void onPostExecute(Integer result) { + super.onPostExecute(result); + + Context context = DeviceActivity.this; + + switch (result) { + case SUCCESS: + Toast.makeText(context, R.string.DeviceProvisioningActivity_content_progress_success, Toast.LENGTH_SHORT).show(); + finish(); + return; + case NO_DEVICE: + Toast.makeText(context, R.string.DeviceProvisioningActivity_content_progress_no_device, Toast.LENGTH_LONG).show(); + break; + case NETWORK_ERROR: + Toast.makeText(context, R.string.DeviceProvisioningActivity_content_progress_network_error, Toast.LENGTH_LONG).show(); + break; + case KEY_ERROR: + Toast.makeText(context, R.string.DeviceProvisioningActivity_content_progress_key_error, Toast.LENGTH_LONG).show(); + break; + case LIMIT_EXCEEDED: + Toast.makeText(context, R.string.DeviceProvisioningActivity_sorry_you_have_too_many_devices_linked_already, Toast.LENGTH_LONG).show(); + break; + } + + getSupportFragmentManager().popBackStackImmediate(); + } + }.execute(); + } +} diff --git a/src/org/thoughtcrime/securesms/DeviceAddFragment.java b/src/org/thoughtcrime/securesms/DeviceAddFragment.java new file mode 100644 index 0000000000..a4de3916b2 --- /dev/null +++ b/src/org/thoughtcrime/securesms/DeviceAddFragment.java @@ -0,0 +1,220 @@ +package org.thoughtcrime.securesms; + +import android.animation.Animator; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewAnimationUtils; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.PlanarYUVLuminanceSource; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.qrcode.QRCodeReader; + +import org.thoughtcrime.securesms.components.camera.CameraView; +import org.thoughtcrime.securesms.components.camera.CameraView.PreviewCallback; +import org.thoughtcrime.securesms.components.camera.CameraView.PreviewFrame; +import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.ViewUtil; + +public class DeviceAddFragment extends Fragment implements PreviewCallback { + + private static final String TAG = DeviceAddFragment.class.getSimpleName(); + + private final QRCodeReader reader = new QRCodeReader(); + + private ViewGroup container; + private LinearLayout overlay; + private ImageView devicesImage; + private CameraView scannerView; + private PreviewFrame previewFrame; + private ScanningThread scanningThread; + private ScanListener scanListener; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) { + this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment); + this.overlay = ViewUtil.findById(this.container, R.id.overlay); + this.scannerView = ViewUtil.findById(this.container, R.id.scanner); + this.devicesImage = ViewUtil.findById(this.container, R.id.devices); + this.scannerView.onResume(); + this.scannerView.setPreviewCallback(this); + + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + this.overlay.setOrientation(LinearLayout.HORIZONTAL); + } else { + this.overlay.setOrientation(LinearLayout.VERTICAL); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + this.container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) + { + v.removeOnLayoutChangeListener(this); + + Animator reveal = ViewAnimationUtils.createCircularReveal(v, right, bottom, 0, (int) Math.hypot(right, bottom)); + reveal.setInterpolator(new DecelerateInterpolator(2f)); + reveal.setDuration(800); + reveal.start(); + } + }); + } + + return this.container; + } + + @Override + public void onResume() { + super.onResume(); + this.scannerView.onResume(); + this.scannerView.setPreviewCallback(this); + this.previewFrame = null; + this.scanningThread = new ScanningThread(); + this.scanningThread.start(); + } + + @Override + public void onPause() { + super.onPause(); + this.scannerView.onPause(); + this.scanningThread.stopScanning(); + } + + @Override + public void onConfigurationChanged(Configuration newConfiguration) { + super.onConfigurationChanged(newConfiguration); + + this.scannerView.onPause(); + + if (newConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + overlay.setOrientation(LinearLayout.HORIZONTAL); + } else { + overlay.setOrientation(LinearLayout.VERTICAL); + } + + this.scannerView.onResume(); + this.scannerView.setPreviewCallback(this); + } + + @Override + public void onPreviewFrame(@NonNull PreviewFrame previewFrame) { + Context context = getActivity(); + + try { + if (context != null) { + synchronized (this) { + this.previewFrame = previewFrame; + this.notify(); + } + } + } catch (RuntimeException e) { + Log.w(TAG, e); + } + } + + public ImageView getDevicesImage() { + return devicesImage; + } + + public void setScanListener(ScanListener scanListener) { + this.scanListener = scanListener; + } + + private class ScanningThread extends Thread { + + private boolean scanning = true; + + @Override + public void run() { + while (true) { + PreviewFrame ourFrame; + + synchronized (DeviceAddFragment.this) { + while (scanning && previewFrame == null) { + Util.wait(DeviceAddFragment.this, 0); + } + + if (!scanning) return; + else ourFrame = previewFrame; + + previewFrame = null; + } + + String url = getUrl(ourFrame.getData(), ourFrame.getWidth(), ourFrame.getHeight(), ourFrame.getOrientation()); + + if (url != null && scanListener != null) { + Uri uri = Uri.parse(url); + scanListener.onUrlFound(uri); + return; + } + } + } + + public void stopScanning() { + synchronized (DeviceAddFragment.this) { + scanning = false; + DeviceAddFragment.this.notify(); + } + } + + private @Nullable String getUrl(byte[] data, int width, int height, int orientation) { + try { + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + byte[] rotatedData = new byte[data.length]; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + rotatedData[x * height + height - y - 1] = data[x + y * width]; + } + } + + int tmp = width; + width = height; + height = tmp; + data = rotatedData; + } + + PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(data, width, height, + 0, 0, width, height, + false); + + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + + Result result = reader.decode(bitmap); + + if (result != null) return result.getText(); + + } catch (NullPointerException | ChecksumException | FormatException e) { + Log.w(TAG, e); + } catch (NotFoundException e) { + // Thanks ZXing... + } + + return null; + } + } + + public interface ScanListener { + public void onUrlFound(Uri uri); + } +} diff --git a/src/org/thoughtcrime/securesms/DeviceLinkFragment.java b/src/org/thoughtcrime/securesms/DeviceLinkFragment.java new file mode 100644 index 0000000000..21487e2138 --- /dev/null +++ b/src/org/thoughtcrime/securesms/DeviceLinkFragment.java @@ -0,0 +1,56 @@ +package org.thoughtcrime.securesms; + +import android.content.res.Configuration; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +public class DeviceLinkFragment extends Fragment implements View.OnClickListener { + + private LinearLayout container; + private LinkClickedListener linkClickedListener; + private Uri uri; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) { + this.container = (LinearLayout) inflater.inflate(R.layout.device_link_fragment, container, false); + this.container.findViewById(R.id.link_device).setOnClickListener(this); + + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + container.setOrientation(LinearLayout.HORIZONTAL); + } else { + container.setOrientation(LinearLayout.VERTICAL); + } + + return this.container; + } + + @Override + public void onConfigurationChanged(Configuration newConfiguration) { + if (newConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + container.setOrientation(LinearLayout.HORIZONTAL); + } else { + container.setOrientation(LinearLayout.VERTICAL); + } + } + + public void setLinkClickedListener(Uri uri, LinkClickedListener linkClickedListener) { + this.uri = uri; + this.linkClickedListener = linkClickedListener; + } + + @Override + public void onClick(View v) { + if (linkClickedListener != null) { + linkClickedListener.onLink(uri); + } + } + + public interface LinkClickedListener { + public void onLink(Uri uri); + } +} diff --git a/src/org/thoughtcrime/securesms/DeviceListActivity.java b/src/org/thoughtcrime/securesms/DeviceListActivity.java deleted file mode 100644 index effd2091b8..0000000000 --- a/src/org/thoughtcrime/securesms/DeviceListActivity.java +++ /dev/null @@ -1,215 +0,0 @@ -package org.thoughtcrime.securesms; - -import android.app.Activity; -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.ListFragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.Loader; -import android.support.v7.app.AlertDialog; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.Toast; - -import org.thoughtcrime.securesms.crypto.MasterSecret; -import org.thoughtcrime.securesms.database.loaders.DeviceListLoader; -import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.util.DynamicLanguage; -import org.thoughtcrime.securesms.util.DynamicTheme; -import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.textsecure.api.TextSecureAccountManager; -import org.whispersystems.textsecure.api.messages.multidevice.DeviceInfo; - -import java.io.IOException; -import java.util.List; - -import javax.inject.Inject; - -public class DeviceListActivity extends PassphraseRequiredActionBarActivity { - - - private final DynamicTheme dynamicTheme = new DynamicTheme(); - private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); - - @Override - public void onPreCreate() { - dynamicTheme.onCreate(this); - dynamicLanguage.onCreate(this); - } - - - @Override - public void onCreate(Bundle bundle, @NonNull MasterSecret masterSecret) { - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - initFragment(android.R.id.content, new DeviceListFragment(), masterSecret); - } - - @Override - public void onResume() { - super.onResume(); - dynamicTheme.onResume(this); - dynamicLanguage.onResume(this); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: finish(); return true; - } - - return false; - } - - public static class DeviceListFragment extends ListFragment - implements LoaderManager.LoaderCallbacks>, ListView.OnItemClickListener, InjectableType - { - - private static final String TAG = DeviceListFragment.class.getSimpleName(); - - @Inject TextSecureAccountManager accountManager; - - private View empty; - private View progressContainer; - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - ApplicationContext.getInstance(activity).injectDependencies(this); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { - View view = inflater.inflate(R.layout.device_list_fragment, container, false); - - this.empty = view.findViewById(R.id.empty); - this.progressContainer = view.findViewById(R.id.progress_container); - - return view; - } - - @Override - public void onActivityCreated(Bundle bundle) { - super.onActivityCreated(bundle); - getLoaderManager().initLoader(0, null, this).forceLoad(); - getListView().setOnItemClickListener(this); - } - - @Override - public Loader> onCreateLoader(int id, Bundle args) { - empty.setVisibility(View.GONE); - progressContainer.setVisibility(View.VISIBLE); - - return new DeviceListLoader(getActivity(), accountManager); - } - - @Override - public void onLoadFinished(Loader> loader, List data) { - progressContainer.setVisibility(View.GONE); - - if (data == null) { - handleLoaderFailed(); - return; - } - - setListAdapter(new DeviceListAdapter(getActivity(), R.layout.device_list_item_view, data)); - - if (data.isEmpty()) { - empty.setVisibility(View.VISIBLE); - TextSecurePreferences.setMultiDevice(getActivity(), false); - } else { - empty.setVisibility(View.GONE); - } - } - - @Override - public void onLoaderReset(Loader> loader) { - setListAdapter(null); - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - final String deviceName = ((DeviceListItem)view).getDeviceName(); - final long deviceId = ((DeviceListItem)view).getDeviceId(); - - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(getActivity().getString(R.string.DeviceListActivity_unlink_s, deviceName)); - builder.setMessage(R.string.DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive); - builder.setNegativeButton(android.R.string.cancel, null); - builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - handleDisconnectDevice(deviceId); - } - }); - builder.show(); - } - - private void handleLoaderFailed() { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setMessage(R.string.DeviceListActivity_network_connection_failed); - builder.setPositiveButton(R.string.DeviceListActivity_try_again, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - getLoaderManager().initLoader(0, null, DeviceListFragment.this); - } - }); - builder.show(); - } - - private void handleDisconnectDevice(final long deviceId) { - new ProgressDialogAsyncTask(getActivity(), - R.string.DeviceListActivity_unlinking_device_no_ellipsis, - R.string.DeviceListActivity_unlinking_device) - { - @Override - protected Void doInBackground(Void... params) { - try { - accountManager.removeDevice(deviceId); - } catch (IOException e) { - Log.w(TAG, e); - Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show(); - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - getLoaderManager().restartLoader(0, null, DeviceListFragment.this); - } - }.execute(); - } - - private static class DeviceListAdapter extends ArrayAdapter { - - private final int resource; - - public DeviceListAdapter(Context context, int resource, List objects) { - super(context, resource, objects); - this.resource = resource; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = ((Activity)getContext()).getLayoutInflater().inflate(resource, parent, false); - } - - ((DeviceListItem)convertView).set(getItem(position)); - - return convertView; - } - } - } - -} diff --git a/src/org/thoughtcrime/securesms/DeviceListFragment.java b/src/org/thoughtcrime/securesms/DeviceListFragment.java new file mode 100644 index 0000000000..23d8b19eff --- /dev/null +++ b/src/org/thoughtcrime/securesms/DeviceListFragment.java @@ -0,0 +1,192 @@ +package org.thoughtcrime.securesms; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ListView; +import android.widget.Toast; + +import com.melnykov.fab.FloatingActionButton; + +import org.thoughtcrime.securesms.database.loaders.DeviceListLoader; +import org.thoughtcrime.securesms.dependencies.InjectableType; +import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.ViewUtil; +import org.whispersystems.textsecure.api.TextSecureAccountManager; +import org.whispersystems.textsecure.api.messages.multidevice.DeviceInfo; + +import java.io.IOException; +import java.util.List; + +import javax.inject.Inject; + +public class DeviceListFragment extends ListFragment + implements LoaderManager.LoaderCallbacks>, + ListView.OnItemClickListener, InjectableType, Button.OnClickListener +{ + + private static final String TAG = DeviceListFragment.class.getSimpleName(); + + @Inject + TextSecureAccountManager accountManager; + + private View empty; + private View progressContainer; + private FloatingActionButton addDeviceButton; + private Button.OnClickListener addDeviceButtonListener; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + ApplicationContext.getInstance(activity).injectDependencies(this); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { + View view = inflater.inflate(R.layout.device_list_fragment, container, false); + + this.empty = view.findViewById(R.id.empty); + this.progressContainer = view.findViewById(R.id.progress_container); + this.addDeviceButton = ViewUtil.findById(view, R.id.add_device); + this.addDeviceButton.setOnClickListener(this); + + return view; + } + + @Override + public void onActivityCreated(Bundle bundle) { + super.onActivityCreated(bundle); + getLoaderManager().initLoader(0, null, this).forceLoad(); + getListView().setOnItemClickListener(this); + } + + public void setAddDeviceButtonListener(Button.OnClickListener listener) { + this.addDeviceButtonListener = listener; + } + + @Override + public Loader> onCreateLoader(int id, Bundle args) { + empty.setVisibility(View.GONE); + progressContainer.setVisibility(View.VISIBLE); + + return new DeviceListLoader(getActivity(), accountManager); + } + + @Override + public void onLoadFinished(Loader> loader, List data) { + progressContainer.setVisibility(View.GONE); + + if (data == null) { + handleLoaderFailed(); + return; + } + + setListAdapter(new DeviceListAdapter(getActivity(), R.layout.device_list_item_view, data)); + + if (data.isEmpty()) { + empty.setVisibility(View.VISIBLE); + TextSecurePreferences.setMultiDevice(getActivity(), false); + } else { + empty.setVisibility(View.GONE); + } + } + + @Override + public void onLoaderReset(Loader> loader) { + setListAdapter(null); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final String deviceName = ((DeviceListItem)view).getDeviceName(); + final long deviceId = ((DeviceListItem)view).getDeviceId(); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(getActivity().getString(R.string.DeviceListActivity_unlink_s, deviceName)); + builder.setMessage(R.string.DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive); + builder.setNegativeButton(android.R.string.cancel, null); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + handleDisconnectDevice(deviceId); + } + }); + builder.show(); + } + + private void handleLoaderFailed() { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage(R.string.DeviceListActivity_network_connection_failed); + builder.setPositiveButton(R.string.DeviceListActivity_try_again, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + getLoaderManager().initLoader(0, null, DeviceListFragment.this); + } + }); + builder.show(); + } + + private void handleDisconnectDevice(final long deviceId) { + new ProgressDialogAsyncTask(getActivity(), + R.string.DeviceListActivity_unlinking_device_no_ellipsis, + R.string.DeviceListActivity_unlinking_device) + { + @Override + protected Void doInBackground(Void... params) { + try { + accountManager.removeDevice(deviceId); + } catch (IOException e) { + Log.w(TAG, e); + Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show(); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + getLoaderManager().restartLoader(0, null, DeviceListFragment.this); + } + }.execute(); + } + + @Override + public void onClick(View v) { + if (addDeviceButtonListener != null) addDeviceButtonListener.onClick(v); + } + + private static class DeviceListAdapter extends ArrayAdapter { + + private final int resource; + + public DeviceListAdapter(Context context, int resource, List objects) { + super(context, resource, objects); + this.resource = resource; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = ((Activity)getContext()).getLayoutInflater().inflate(resource, parent, false); + } + + ((DeviceListItem)convertView).set(getItem(position)); + + return convertView; + } + } +} diff --git a/src/org/thoughtcrime/securesms/components/ShapeScrim.java b/src/org/thoughtcrime/securesms/components/ShapeScrim.java new file mode 100644 index 0000000000..20055c0599 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/ShapeScrim.java @@ -0,0 +1,107 @@ +package org.thoughtcrime.securesms.components; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; + +import org.thoughtcrime.securesms.R; + +public class ShapeScrim extends View { + + private enum ShapeType { + CIRCLE, SQUARE + } + + private final Paint eraser; + private final ShapeType shape; + private final float radius; + + private Bitmap scrim; + private Canvas scrimCanvas; + + public ShapeScrim(Context context) { + this(context, null); + } + + public ShapeScrim(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ShapeScrim(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + if (attrs != null) { + TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ShapeScrim, 0, 0); + String shapeName = typedArray.getString(R.styleable.ShapeScrim_shape); + + if ("square".equalsIgnoreCase(shapeName)) this.shape = ShapeType.SQUARE; + else if ("circle".equalsIgnoreCase(shapeName)) this.shape = ShapeType.CIRCLE; + else this.shape = ShapeType.SQUARE; + + this.radius = typedArray.getFloat(R.styleable.ShapeScrim_radius, 0.4f); + + typedArray.recycle(); + } else { + this.shape = ShapeType.SQUARE; + this.radius = 0.4f; + } + + this.eraser = new Paint(); + this.eraser.setColor(0xFFFFFFFF); + this.eraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + + int shortDimension = getWidth() < getHeight() ? getWidth() : getHeight(); + float drawRadius = shortDimension * radius; + + if (scrimCanvas == null) { + scrim = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); + scrimCanvas = new Canvas(scrim); + } + + scrim.eraseColor(Color.TRANSPARENT); + scrimCanvas.drawColor(Color.parseColor("#55BDBDBD")); + + if (shape == ShapeType.CIRCLE) drawCircle(scrimCanvas, drawRadius, eraser); + else drawSquare(scrimCanvas, drawRadius, eraser); + + canvas.drawBitmap(scrim, 0, 0, null); + } + + @Override + public void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldHeight, oldHeight); + + if (width != oldWidth || height != oldHeight) { + scrim = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + scrimCanvas = new Canvas(scrim); + } + } + + private void drawCircle(Canvas canvas, float radius, Paint eraser) { + canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, eraser); + } + + private void drawSquare(Canvas canvas, float radius, Paint eraser) { + float left = (getWidth() / 2 ) - radius; + float top = (getHeight() / 2) - radius; + float right = left + (radius * 2); + float bottom = top + (radius * 2); + + RectF square = new RectF(left, top, right, bottom); + + canvas.drawRoundRect(square, 25, 25, eraser); + } +} diff --git a/src/org/thoughtcrime/securesms/components/camera/CameraUtils.java b/src/org/thoughtcrime/securesms/components/camera/CameraUtils.java index 028842d1ef..65ff7ae33d 100644 --- a/src/org/thoughtcrime/securesms/components/camera/CameraUtils.java +++ b/src/org/thoughtcrime/securesms/components/camera/CameraUtils.java @@ -7,6 +7,7 @@ import android.hardware.Camera.Size; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.DisplayMetrics; +import android.util.Log; import android.view.Surface; import java.util.Collections; @@ -22,6 +23,7 @@ public class CameraUtils { int width, int height, @NonNull Camera camera) { + Log.w("CameraUtils", String.format("getPreferredPreviewSize(%d, %d, %d)", displayOrientation, width, height)); double targetRatio = (double)width / height; Size optimalSize = null; double minDiff = Double.MAX_VALUE; diff --git a/src/org/thoughtcrime/securesms/components/camera/CameraView.java b/src/org/thoughtcrime/securesms/components/camera/CameraView.java index 95ed524e14..32bc036987 100644 --- a/src/org/thoughtcrime/securesms/components/camera/CameraView.java +++ b/src/org/thoughtcrime/securesms/components/camera/CameraView.java @@ -19,6 +19,7 @@ import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.pm.ActivityInfo; +import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Rect; import android.hardware.Camera; @@ -39,6 +40,7 @@ import java.io.IOException; import java.util.List; import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; @@ -73,7 +75,15 @@ public class CameraView extends FrameLayout { super(context, attrs, defStyle); setBackgroundColor(Color.BLACK); - if (isMultiCamera()) cameraId = TextSecurePreferences.getDirectCaptureCameraId(context); + if (attrs != null) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CameraView); + int camera = typedArray.getInt(R.styleable.CameraView_camera, -1); + + if (camera != -1) cameraId = camera; + else if (isMultiCamera()) cameraId = TextSecurePreferences.getDirectCaptureCameraId(context); + + typedArray.recycle(); + } surface = new CameraSurfaceView(getContext()); onOrientationChange = new OnOrientationChange(context.getApplicationContext()); @@ -87,7 +97,9 @@ public class CameraView extends FrameLayout { Log.w(TAG, "onResume() queued"); enqueueTask(new SerialAsyncTask() { @Override - protected @Nullable Camera onRunBackground() { + protected + @Nullable + Camera onRunBackground() { try { return Camera.open(cameraId); } catch (Exception e) { @@ -131,15 +143,19 @@ public class CameraView extends FrameLayout { enqueueTask(new SerialAsyncTask() { private Optional cameraToDestroy; - @Override protected void onPreMain() { + + @Override + protected void onPreMain() { cameraToDestroy = camera; camera = Optional.absent(); } - @Override protected Void onRunBackground() { + @Override + protected Void onRunBackground() { if (cameraToDestroy.isPresent()) { try { stopPreview(); + cameraToDestroy.get().setPreviewCallback(null); cameraToDestroy.get().release(); Log.w(TAG, "released old camera instance"); } catch (Exception e) { @@ -224,6 +240,30 @@ public class CameraView extends FrameLayout { this.listener = listener; } + public void setPreviewCallback(final PreviewCallback previewCallback) { + enqueueTask(new PostInitializationTask() { + @Override + protected void onPostMain(Void avoid) { + if (camera.isPresent()) { + camera.get().setPreviewCallback(new Camera.PreviewCallback() { + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + if (!CameraView.this.camera.isPresent()) { + return; + } + + final int rotation = getCameraPictureOrientation(); + final Size previewSize = camera.getParameters().getPreviewSize(); + if (data != null) { + previewCallback.onPreviewFrame(new PreviewFrame(data, previewSize.width, previewSize.height, rotation)); + } + } + }); + } + } + }); + } + public boolean isMultiCamera() { return Camera.getNumberOfCameras() > 1; } @@ -515,4 +555,38 @@ public class CameraView extends FrameLayout { void onImageCapture(@NonNull final byte[] imageBytes); void onCameraFail(); } + + public interface PreviewCallback { + void onPreviewFrame(@NonNull PreviewFrame frame); + } + + public static class PreviewFrame { + private final @NonNull byte[] data; + private final int width; + private final int height; + private final int orientation; + + private PreviewFrame(@NonNull byte[] data, int width, int height, int orientation) { + this.data = data; + this.width = width; + this.height = height; + this.orientation = orientation; + } + + public @NonNull byte[] getData() { + return data; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getOrientation() { + return orientation; + } + } } diff --git a/src/org/thoughtcrime/securesms/dependencies/TextSecureCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/TextSecureCommunicationModule.java index 0d999b5728..2a38e2b3a9 100644 --- a/src/org/thoughtcrime/securesms/dependencies/TextSecureCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/TextSecureCommunicationModule.java @@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.dependencies; import android.content.Context; import org.thoughtcrime.securesms.BuildConfig; -import org.thoughtcrime.securesms.DeviceListActivity; +import org.thoughtcrime.securesms.DeviceListFragment; import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore; import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; import org.thoughtcrime.securesms.jobs.CleanPreKeysJob; @@ -43,7 +43,7 @@ import dagger.Provides; PushNotificationReceiveJob.class, MultiDeviceContactUpdateJob.class, MultiDeviceGroupUpdateJob.class, - DeviceListActivity.DeviceListFragment.class, + DeviceListFragment.class, RefreshAttributesJob.class, GcmRefreshJob.class}) public class TextSecureCommunicationModule {