Device provisioning

Closes #4553
// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-11-19 10:21:19 -08:00
parent 02c37e815c
commit 7c0bf0c871
38 changed files with 1047 additions and 246 deletions

View File

@ -131,12 +131,6 @@
<activity android:name=".DeviceProvisioningActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tsdevice"/>
</intent-filter>
</activity>
<activity android:name=".preferences.MmsPreferencesActivity"
@ -266,7 +260,7 @@
<activity android:name=".RegistrationProgressActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DeviceListActivity"
<activity android:name=".DeviceActivity"
android:label="@string/AndroidManifest_manage_linked_devices"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>

View File

@ -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',

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<org.thoughtcrime.securesms.components.camera.CameraView
android:id="@+id/scanner"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:camera="0"/>
<LinearLayout android:id="@+id/overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:weightSum="2">
<org.thoughtcrime.securesms.components.ShapeScrim
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<LinearLayout android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:background="@color/gray5"
android:gravity="center">
<ImageView android:id="@+id/devices"
android:src="@drawable/ic_devices_white"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/gray27"
android:transitionName="devices"
android:layout_marginBottom="16dp"/>
<TextView android:text="@string/device_add_fragment__scan_the_qr_code_displayed_on_the_device_to_link"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_devices_white"
android:transitionName="devices"
android:tint="@color/gray27"
android:layout_marginBottom="25dp"
android:contentDescription="@string/device_link_fragment__link_device"/>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="25dp"
android:layout_marginRight="25dp">
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:textStyle="bold"
android:text="@string/DeviceProvisioningActivity_link_this_device"
android:textSize="16sp"/>
<View android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#1E000000"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@string/DeviceProvisioningActivity_content_intro"
android:textSize="14sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@string/DeviceProvisioningActivity_content_bullets"
android:textSize="14sp"/>
<View android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#1E000000"/>
<LinearLayout android:id="@+id/link_device"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:gravity="center_vertical"
android:clickable="true">
<ImageView android:id="@+id/check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="16dp"
android:src="@drawable/ic_check_white_24dp"
android:tint="@color/blue_400"
android:clickable="false"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/blue_400"
android:text="@string/device_link_fragment__link_device"
android:clickable="false"/>
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
</LinearLayout>

View File

@ -2,9 +2,9 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="16dip"
android:paddingRight="16dip">
xmlns:tools="http://schemas.android.com/tools"
xmlns:fab="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<LinearLayout android:id="@+id/progress_container"
android:layout_width="fill_parent"
@ -27,13 +27,32 @@
android:gravity="center|center_vertical"
android:textSize="20sp"
android:visibility="gone"
android:text="@string/device_list_fragment__no_devices_linked"/>
android:text="@string/device_list_fragment__no_devices_linked"
android:paddingLeft="16dip"
android:paddingRight="16dip"
android:layout_weight="1"
tools:visibility="visible"/>
<ListView android:id="@id/android:list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:drawSelectorOnTop="false"/>
android:drawSelectorOnTop="false"
android:paddingLeft="16dip"
android:paddingRight="16dip"
tools:visibility="gone"/>
<com.melnykov.fab.FloatingActionButton
android:id="@+id/add_device"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_margin="16dp"
android:src="@drawable/ic_add_white_original_24dp"
android:focusable="true"
android:contentDescription="@string/device_list_fragment__link_new_device"
fab:fab_colorNormal="?fab_color"
fab:fab_colorPressed="@color/textsecure_primary_dark"
fab:fab_colorRipple="@color/textsecure_primary_dark" />
</LinearLayout>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeTransform />
<changeBounds />
</transitionSet>

View File

@ -147,4 +147,13 @@
<attr name="circleColor" format="color"/>
</declare-styleable>
<declare-styleable name="ShapeScrim">
<attr name="shape" format="string"/>
<attr name="radius" format="float"/>
</declare-styleable>
<declare-styleable name="CameraView">
<attr name="camera" format="integer"/>
</declare-styleable>
</resources>

View File

@ -345,12 +345,12 @@
<string name="DeviceProvisioningActivity_title">Link this device?</string>
<string name="DeviceProvisioningActivity_content_intro">It will be able to</string>
<string name="DeviceProvisioningActivity_content_bullets">
- Read all your messages
\n- Send messages in your name
Read all your messages
\n Send messages in your name
</string>
<string name="DeviceProvisioningActivity_content_progress_title">Linking device</string>
<string name="DeviceProvisioningActivity_content_progress_content">Linking new device...</string>
<string name="DeviceProvisioningActivity_content_progress_success">Device linked!</string>
<string name="DeviceProvisioningActivity_content_progress_success">Device approved!</string>
<string name="DeviceProvisioningActivity_content_progress_no_device">No device found.</string>
<string name="DeviceProvisioningActivity_content_progress_network_error">Network error.</string>
<string name="DeviceProvisioningActivity_content_progress_key_error">Invalid QR code.</string>
@ -1128,6 +1128,9 @@
<!-- transport_selection_list_item -->
<string name="transport_selection_list_item__transport_icon">Transport icon</string>
<string name="device_add_fragment__scan_the_qr_code_displayed_on_the_device_to_link">Scan the QR code displayed on the device to link</string>
<string name="device_link_fragment__link_device">Link device</string>
<string name="device_list_fragment__link_new_device">Link new device</string>
<!-- EOF -->

View File

@ -188,7 +188,7 @@
<item name="pref_ic_app_protection">@drawable/ic_app_protection_black</item>
<item name="pref_ic_appearance">@drawable/ic_brightness_6_black</item>
<item name="pref_ic_chats">@drawable/ic_forum_black_32dp</item>
<item name="pref_ic_devices">@drawable/ic_devices_black_48dp</item>
<item name="pref_ic_devices">@drawable/ic_laptop_black_32dp</item>
<item name="pref_ic_advanced">@drawable/ic_advanced_black</item>
<item name="app_protect_timeout_picker_color">@style/BetterPickersDialogFragment.Light</item>
@ -301,7 +301,7 @@
<item name="pref_ic_app_protection">@drawable/ic_app_protection_gray</item>
<item name="pref_ic_appearance">@drawable/ic_brightness_6_gray</item>
<item name="pref_ic_chats">@drawable/ic_forum_grey_32dp</item>
<item name="pref_ic_devices">@drawable/ic_devices_grey600_48dp</item>
<item name="pref_ic_devices">@drawable/ic_laptop_light_32dp</item>
<item name="pref_ic_advanced">@drawable/ic_advanced_gray</item>
<item name="app_protect_timeout_picker_color">@style/BetterPickersDialogFragment</item>

View File

@ -21,11 +21,9 @@
android:title="@string/preferences__chats"
android:icon="?pref_ic_chats"/>
<!--<Preference android:key="preference_category_devices"-->
<!--android:title="Devices"-->
<!--android:icon="?pref_ic_devices">-->
<!--</Preference>-->
<Preference android:key="preference_category_devices"
android:title="Devices"
android:icon="?pref_ic_devices"/>
<Preference android:key="preference_category_advanced"
android:title="@string/preferences__advanced"

View File

@ -138,8 +138,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
.setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_APPEARANCE));
this.findPreference(PREFERENCE_CATEGORY_CHATS)
.setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_CHATS));
// this.findPreference(PREFERENCE_CATEGORY_DEVICES)
// .setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_DEVICES));
this.findPreference(PREFERENCE_CATEGORY_DEVICES)
.setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_DEVICES));
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
.setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_ADVANCED));
}
@ -194,7 +194,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
fragment = new ChatsPreferenceFragment();
break;
case PREFERENCE_CATEGORY_DEVICES:
Intent intent = new Intent(getActivity(), DeviceListActivity.class);
Intent intent = new Intent(getActivity(), DeviceActivity.class);
startActivity(intent);
break;
case PREFERENCE_CATEGORY_ADVANCED:

View File

@ -0,0 +1,194 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Vibrator;
import android.support.annotation.NonNull;
import android.transition.TransitionInflater;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.push.TextSecureCommunicationFactory;
import org.thoughtcrime.securesms.util.Base64;
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.thoughtcrime.securesms.util.Util;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.textsecure.api.TextSecureAccountManager;
import org.whispersystems.textsecure.api.push.exceptions.NotFoundException;
import org.whispersystems.textsecure.internal.push.DeviceLimitExceededException;
import java.io.IOException;
public class DeviceActivity extends PassphraseRequiredActionBarActivity
implements Button.OnClickListener, DeviceAddFragment.ScanListener, DeviceLinkFragment.LinkClickedListener
{
private static final String TAG = DeviceActivity.class.getSimpleName();
private final DynamicTheme dynamicTheme = new DynamicTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private DeviceAddFragment deviceAddFragment;
private DeviceListFragment deviceListFragment;
private DeviceLinkFragment deviceLinkFragment;
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this);
}
@Override
public void onCreate(Bundle bundle, @NonNull MasterSecret masterSecret) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
this.deviceAddFragment = new DeviceAddFragment();
this.deviceListFragment = new DeviceListFragment();
this.deviceLinkFragment = new DeviceLinkFragment();
this.deviceListFragment.setAddDeviceButtonListener(this);
this.deviceAddFragment.setScanListener(this);
initFragment(android.R.id.content, 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;
}
@Override
public void onClick(View v) {
getSupportFragmentManager().beginTransaction()
.replace(android.R.id.content, deviceAddFragment)
.addToBackStack(null)
.commit();
}
@Override
public void onUrlFound(final Uri uri) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
deviceLinkFragment.setLinkClickedListener(uri, DeviceActivity.this);
if (Build.VERSION.SDK_INT >= 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<Void, Void, Integer>(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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<List<DeviceInfo>>, 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<List<DeviceInfo>> onCreateLoader(int id, Bundle args) {
empty.setVisibility(View.GONE);
progressContainer.setVisibility(View.VISIBLE);
return new DeviceListLoader(getActivity(), accountManager);
}
@Override
public void onLoadFinished(Loader<List<DeviceInfo>> loader, List<DeviceInfo> 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<List<DeviceInfo>> 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<Void, Void, Void>(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<DeviceInfo> {
private final int resource;
public DeviceListAdapter(Context context, int resource, List<DeviceInfo> 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;
}
}
}
}

View File

@ -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<List<DeviceInfo>>,
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<List<DeviceInfo>> onCreateLoader(int id, Bundle args) {
empty.setVisibility(View.GONE);
progressContainer.setVisibility(View.VISIBLE);
return new DeviceListLoader(getActivity(), accountManager);
}
@Override
public void onLoadFinished(Loader<List<DeviceInfo>> loader, List<DeviceInfo> 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<List<DeviceInfo>> 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<Void, Void, Void>(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<DeviceInfo> {
private final int resource;
public DeviceListAdapter(Context context, int resource, List<DeviceInfo> 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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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<Camera>() {
@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<Void>() {
private Optional<Camera> 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<Void>() {
@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;
}
}
}

View File

@ -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 {