From bf3c1d37459bd3d3a8146bfdc4624a9b466ef133 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Fri, 19 Jun 2015 22:02:10 -0700 Subject: [PATCH] Support for device management, limits, and contact requests. // FREEBIE --- AndroidManifest.xml | 4 + build.gradle | 2 +- res/drawable-hdpi/ic_devices_black_48dp.png | Bin 0 -> 346 bytes res/drawable-hdpi/ic_devices_grey600_48dp.png | Bin 0 -> 352 bytes res/drawable-mdpi/ic_devices_black_48dp.png | Bin 0 -> 280 bytes res/drawable-mdpi/ic_devices_grey600_48dp.png | Bin 0 -> 287 bytes res/drawable-xhdpi/ic_devices_black_48dp.png | Bin 0 -> 420 bytes .../ic_devices_grey600_48dp.png | Bin 0 -> 430 bytes res/drawable-xxhdpi/ic_devices_black_48dp.png | Bin 0 -> 574 bytes .../ic_devices_grey600_48dp.png | Bin 0 -> 609 bytes .../ic_devices_black_48dp.png | Bin 0 -> 706 bytes .../ic_devices_grey600_48dp.png | Bin 0 -> 763 bytes res/layout/device_list_fragment.xml | 39 ++++ res/layout/device_list_item_view.xml | 33 +++ res/values/attrs.xml | 1 + res/values/strings.xml | 20 ++ res/values/themes.xml | 2 + res/xml/preferences.xml | 6 + .../ApplicationPreferencesActivity.java | 27 ++- .../securesms/DeviceListActivity.java | 211 ++++++++++++++++++ .../securesms/DeviceListItem.java | 62 +++++ .../securesms/DeviceProvisioningActivity.java | 19 +- .../database/loaders/DeviceListLoader.java | 58 +++++ .../TextSecureCommunicationModule.java | 5 +- .../securesms/jobs/PushDecryptJob.java | 12 +- 25 files changed, 482 insertions(+), 19 deletions(-) create mode 100644 res/drawable-hdpi/ic_devices_black_48dp.png create mode 100644 res/drawable-hdpi/ic_devices_grey600_48dp.png create mode 100644 res/drawable-mdpi/ic_devices_black_48dp.png create mode 100644 res/drawable-mdpi/ic_devices_grey600_48dp.png create mode 100644 res/drawable-xhdpi/ic_devices_black_48dp.png create mode 100644 res/drawable-xhdpi/ic_devices_grey600_48dp.png create mode 100644 res/drawable-xxhdpi/ic_devices_black_48dp.png create mode 100644 res/drawable-xxhdpi/ic_devices_grey600_48dp.png create mode 100644 res/drawable-xxxhdpi/ic_devices_black_48dp.png create mode 100644 res/drawable-xxxhdpi/ic_devices_grey600_48dp.png create mode 100644 res/layout/device_list_fragment.xml create mode 100644 res/layout/device_list_item_view.xml create mode 100644 src/org/thoughtcrime/securesms/DeviceListActivity.java create mode 100644 src/org/thoughtcrime/securesms/DeviceListItem.java create mode 100644 src/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 538f960447..3045165bbf 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -192,6 +192,10 @@ + + I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g$s z6uji=;uw-~@9j;0K1M@{_J{HlZ*_26v~b^y-r1Eh>1MHPSe{XEal@^wzn;>!_$u#5 zzB(a%KcL6O#pO%Bn<2(es#Q=Z!T&2 nYSFn@TloK9QdUy>wKuPU|KpAH<4+F&eaGPG>gTe~DWM4fOznmc literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/ic_devices_grey600_48dp.png b/res/drawable-hdpi/ic_devices_grey600_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..64f9eaab15892742e0e92b1b4668b310435dc926 GIT binary patch literal 352 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw6p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g$s z6ujZ-;uw-~@9j-PK1N50qaV%BX1ILGTX4~mH>&kQ?jt^>q{YvL_zq@@u}e%nXEfQ= zTKVVxU%sDCPtWhVY1;a!i;;yxK*6D5R@0hivocoX&F-qY;E>*XOWs@d#H(2ipBcA= z%uFkM%|F>MECGoVWj_Bq_Xp@l22WQ%mvv4FO#q%-gQEZd literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_devices_black_48dp.png b/res/drawable-mdpi/ic_devices_black_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4d47b98396d93f8c8feec0ad3f0b423b13d3354b GIT binary patch literal 280 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tg=CK)Uj~LMH3o);76yi2K%s^g z3=E|}g|8AA7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+;1OBOz`!jG!i)^F=12eq zCwRIzhD02GJN-0ogMxqy`{S#2ImaeMtW52U;o)6l{)pcvg_r$#;gvVt?>;bdC{|bb za0o0d46X~+uG5|oa@xJIv#G)FYy0fY4tidqv(`^`W;8gYK6%T5r0Y*y>f}G%+_3AD zi04zeEw_YU|4Hr9WEBbcz};B6@u*C=e--gB78W```n4SYddO{UNE2f*Jd+}DQ1SaL#i>QyagR;pSmFZn4%SDp3*Oa9IHG5l zb#MjiVzHhm11Fsy{*jwo3JPtr7|-ff998IYzv_KT_e1)OqY7=OE1smC&JfSK(ljxf ZkwGXrp(?mM!35|;22WQ%mvv4FO#nuqTE_qY literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/ic_devices_black_48dp.png b/res/drawable-xhdpi/ic_devices_black_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..62946339e1b2ad38b0aced34c6362120195849f8 GIT binary patch literal 420 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcg6p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g&? zz`!Wu>EaktaqI0YL%t>hk+ynX!;2@SEhHbEc=F^>s>OuW=@VY0_3WM;e$Yi?PC@7j z3GXc?HS*F+p4Kj`{jzi_&{PJ71?ze4*>z6QxaOmruT+<`ruS4|oG4TB5B9Y+l1|Qt z_inX+INL2`Nw>e?_5BWy!o^l`AKAD3V)b*O^yDL@jhEEkmuYiX+OZ2bFfg+GQrmDL z?&$T?uVSoQm%X!Fv%g;8-_>1aCw#d6i{03+l^^uoyVGg1*xKUnOsbI_jLTW%rbP4> zw<)$TFmWg}U=k4qKMu?JeC=Jo!5%DoA-{1^=<7Rs53CG*z4*6%;fl@6rq!lKo%;H+ q*mGgf-F@$#FI)SJ3FNB=xz7xLF3Db9z|{5##P@Xdb6Mw<&;$VYtet59 literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/ic_devices_grey600_48dp.png b/res/drawable-xhdpi/ic_devices_grey600_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3beebd2e8c167030f929d38373df75c107d31248 GIT binary patch literal 430 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcg6p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g&? zz`&^H>EaktaqI1Edp<@-k>ellcX)KPNNc#RTy*h)8khP3(G{{s=KHz4<#{9^vN~?> z|FUTxAN}5TXU~}$qx^d+){*8D-k#t98V(1X%GKX}V{5mD2EIF<_9l+;iD~HhcU(`l z2A%#SH{;gKf4UBLy*}Py{<3!a#cibyM}JvPsJ>Hf*~thpz~NNjO>_%)r1c48n{Iv*t)J zFfeZOba4!+xb^m~zaDd-%z=;bOC1lfN+l-dCqL%Ny1b+PM=^`|#DteBTR1NrxumPn z8KOGvj7Dw6XS@4)x6f>uzvug{b6@;hR6Hj^(3L+?@q6w@eOV;^`eNtPL#lQerhbo9 zkMZuW3EDj^-P|vE1oRHJEn`-*TxH%*(4L%ANmb>n$AbIXj7ifsu)YLqNfyfn`O+ zf}<8)jy-8*Tp1#NEjn6X&GoDN_kusVH*u12>h_J7SQZ?0vB{r4d&YVB7aA{rUiSETk>~qTkZ}xuAK&V&O}T7$yRTOvme`tom$Tl0Gsx6ge0T2U`~{VVo+@GxGI b_La->*TdqjO`JKvXl3wp^>bP0l+XkK{Hf;= literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_devices_grey600_48dp.png b/res/drawable-xxhdpi/ic_devices_grey600_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f7dbe4396a59e118e458a761c502c0554e332348 GIT binary patch literal 609 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q1xWh(YZ(J6g=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C(G&@^*J&_}|`tWO>_%)r1c48n{Iv*t)J zFfiWqba4!+xb^mqq2C<`5w-{Q;j0~3#1}+xMY(ucFfVbut+vRYYfI-5)@}a18I6;q z_!uWHnxH*F+u>4Ms=@4X zU|NyxaQf$*^_3_7%v{%gCuP=B6-kr20Q2?FHdvhtHRHar_iWy~HrW}rsTGsf#ykuC z>mbklQRU?9nicXdVlTdW|C;@lPW0Zo1xpuQjep&2(gig5SHJPAHli}3 zDvQ{o;M{^PHVBKkELRf5^bg3A70Y3T~IK`x4IoS?RCI z&9i>*Bp;Qk)qKBXe&cjvT-xoF>yL{Vw|zESzh0nsE6a`3p7SIkLmSe6PH<-nD;2tT zI{k5e@}bqN6%+LN?B25M*nFvXkKw;l&H?)Ndp@mLKI;YG2bGPrw|-u_ScMNCa@x~tR3I~D>f$NiTtEGh;QVRv{_^_*zvxS85}{?MIT#oOfQBsqOL)dP2BB!Hy!^M76qTXQ5sR1frVpZ985nj;acdf-58 zzOKXl30aI)Z6Vwnl02@~x0oK?rEjo%-urx>!e8Zwu(+*3d*$@NK4U-Yr|S#*HcvnP z{IuCz`3D!uYW}P3QP@1;uFd?H6SqjFS|?jS`2NHG`OzME58t?_!D0+5C-de_TXXxo z_3{NvroT@;eqmWU`yQPd4%3?~0l&}fFJ<8O3Xf+iSYGnAlHpwoBRE7BH2q=D`hJ;p z^3_|6evX)~!5L&&!x1ca!QTA(Pf5ert=Io>%zj;4Q`QhHfEo5_7cyVIN@lS44!374 zh}dn_(BG=RYF?`Rf&QLT*ZxgVW;j{4Z|a)grt4!_RZm(pzd8HErDWQ7$tNb6ZEtzy z-{0S-`!vvO^Sq~L{OA5wX}@{?;{5ph4E7y@%(&th)xih;-_zgWl)HSwwOwBsfWXt$ K&t;ucLK6TcVgykD literal 0 HcmV?d00001 diff --git a/res/layout/device_list_fragment.xml b/res/layout/device_list_fragment.xml new file mode 100644 index 0000000000..cf7df56a93 --- /dev/null +++ b/res/layout/device_list_fragment.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/device_list_item_view.xml b/res/layout/device_list_item_view.xml new file mode 100644 index 0000000000..d62de7fbc6 --- /dev/null +++ b/res/layout/device_list_item_view.xml @@ -0,0 +1,33 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 9ca060e1fc..1c3d9b7639 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -122,6 +122,7 @@ + diff --git a/res/values/strings.xml b/res/values/strings.xml index 8aa13a829b..eada056c18 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -166,6 +166,20 @@ Now %d min + + Disconnect \'%s\'? + By disconnecting this device, it will no longer be able to send or receive messages. + Network connection failed... + Try again + Disconnecting device.. + Disconnecting device + Network failed! + + + Unnamed device + Created %s + Last active %s + Share with @@ -283,6 +297,7 @@ No device found. Network error. Invalid QR code. + Sorry, you have too many devices registered already, try removing some... Enter passphrase @@ -532,6 +547,9 @@ Loading countries... Search + + No devices paired... + Could not grab logs from your device. You can still use ADB to get debug logs instead. Thanks for your help! @@ -715,6 +733,7 @@ All images All images with %1$s Message Details + Manage paired devices Import / export @@ -959,6 +978,7 @@ Transport icon + diff --git a/res/values/themes.xml b/res/values/themes.xml index 62171cc14c..6ac92c8023 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -173,6 +173,7 @@ @drawable/ic_app_protection_black @drawable/ic_brightness_6_black @drawable/ic_delete_black + @drawable/ic_devices_black_48dp @drawable/ic_advanced_black @style/BetterPickersDialogFragment.Light @@ -297,6 +298,7 @@ @drawable/ic_app_protection_gray @drawable/ic_brightness_6_gray @drawable/ic_delete_gray + @drawable/ic_devices_grey600_48dp @drawable/ic_advanced_gray @style/BetterPickersDialogFragment diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index ff44032dcb..bea5750b3d 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -21,6 +21,12 @@ android:title="@string/preferences__delete_old_messages" android:icon="?pref_ic_storage"/> + + + + diff --git a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java index cef095caec..733ee79da8 100644 --- a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java +++ b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java @@ -55,6 +55,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA private static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection"; private static final String PREFERENCE_CATEGORY_APPEARANCE = "preference_category_appearance"; private static final String PREFERENCE_CATEGORY_STORAGE = "preference_category_storage"; + private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices"; private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced"; private final DynamicTheme dynamicTheme = new DynamicTheme(); @@ -131,6 +132,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA .setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_APPEARANCE)); this.findPreference(PREFERENCE_CATEGORY_STORAGE) .setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_STORAGE)); + this.findPreference(PREFERENCE_CATEGORY_DEVICES) + .setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_DEVICES)); this.findPreference(PREFERENCE_CATEGORY_ADVANCED) .setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_ADVANCED)); } @@ -166,7 +169,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA @Override public boolean onPreferenceClick(Preference preference) { - Fragment fragment; + Fragment fragment = null; switch (category) { case PREFERENCE_CATEGORY_SMS_MMS: @@ -184,6 +187,10 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA case PREFERENCE_CATEGORY_STORAGE: fragment = new StoragePreferenceFragment(); break; + case PREFERENCE_CATEGORY_DEVICES: + Intent intent = new Intent(getActivity(), DeviceListActivity.class); + startActivity(intent); + break; case PREFERENCE_CATEGORY_ADVANCED: fragment = new AdvancedPreferenceFragment(); break; @@ -191,15 +198,17 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA throw new AssertionError(); } - Bundle args = new Bundle(); - args.putParcelable("master_secret", masterSecret); - fragment.setArguments(args); + if (fragment != null) { + Bundle args = new Bundle(); + args.putParcelable("master_secret", masterSecret); + fragment.setArguments(args); - FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); - FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); - fragmentTransaction.replace(android.R.id.content, fragment); - fragmentTransaction.addToBackStack(null); - fragmentTransaction.commit(); + FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + fragmentTransaction.replace(android.R.id.content, fragment); + fragmentTransaction.addToBackStack(null); + fragmentTransaction.commit(); + } return true; } diff --git a/src/org/thoughtcrime/securesms/DeviceListActivity.java b/src/org/thoughtcrime/securesms/DeviceListActivity.java new file mode 100644 index 0000000000..895a4b0c53 --- /dev/null +++ b/src/org/thoughtcrime/securesms/DeviceListActivity.java @@ -0,0 +1,211 @@ +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.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 com.afollestad.materialdialogs.AlertDialogWrapper; + +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.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); + 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(); + + AlertDialogWrapper.Builder builder = new AlertDialogWrapper.Builder(getActivity()); + builder.setTitle(getActivity().getString(R.string.DeviceListActivity_disconnect_s, deviceName)); + builder.setMessage(R.string.DeviceListActivity_by_disconnecting_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() { + AlertDialogWrapper.Builder builder = new AlertDialogWrapper.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_disconnecting_device, + R.string.DeviceListActivity_disconnecting_device_no_ellipse) + { + @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/DeviceListItem.java b/src/org/thoughtcrime/securesms/DeviceListItem.java new file mode 100644 index 0000000000..39b32f5bac --- /dev/null +++ b/src/org/thoughtcrime/securesms/DeviceListItem.java @@ -0,0 +1,62 @@ +package org.thoughtcrime.securesms; + +import android.content.Context; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.thoughtcrime.securesms.util.DateUtils; +import org.whispersystems.textsecure.api.messages.multidevice.DeviceInfo; + +import java.util.Locale; + +public class DeviceListItem extends LinearLayout { + + private long deviceId; + private TextView name; + private TextView created; + private TextView lastActive; + + public DeviceListItem(Context context) { + super(context); + } + + public DeviceListItem(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + this.name = (TextView) findViewById(R.id.name); + this.created = (TextView) findViewById(R.id.created); + this.lastActive = (TextView) findViewById(R.id.active); + } + + public void set(DeviceInfo deviceInfo) { + if (TextUtils.isEmpty(deviceInfo.getName())) this.name.setText(R.string.DeviceListItem_unnamed_device); + else this.name.setText(deviceInfo.getName()); + + this.created.setText(getContext().getString(R.string.DeviceListItem_created_s, + DateUtils.getExtendedRelativeTimeSpanString(getContext(), + Locale.getDefault(), + deviceInfo.getCreated()))); + + this.lastActive.setText(getContext().getString(R.string.DeviceListItem_last_active_s, + DateUtils.getExtendedRelativeTimeSpanString(getContext(), + Locale.getDefault(), + deviceInfo.getLastSeen()))); + + this.deviceId = deviceInfo.getId(); + } + + public long getDeviceId() { + return deviceId; + } + + public String getDeviceName() { + return name.getText().toString(); + } + +} diff --git a/src/org/thoughtcrime/securesms/DeviceProvisioningActivity.java b/src/org/thoughtcrime/securesms/DeviceProvisioningActivity.java index 2749ad5421..4f6149a3f5 100644 --- a/src/org/thoughtcrime/securesms/DeviceProvisioningActivity.java +++ b/src/org/thoughtcrime/securesms/DeviceProvisioningActivity.java @@ -6,13 +6,11 @@ import android.content.DialogInterface.OnDismissListener; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; -import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.util.Log; import android.view.Window; import android.widget.Toast; -import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.MaterialDialog.Builder; import com.afollestad.materialdialogs.MaterialDialog.ButtonCallback; @@ -28,6 +26,7 @@ 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; @@ -94,10 +93,11 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActiv 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 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) { @@ -113,9 +113,13 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActiv accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, verificationCode); 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; @@ -144,6 +148,9 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActiv 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_registered_already, Toast.LENGTH_LONG).show(); + break; } dialog.dismiss(); } diff --git a/src/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java b/src/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java new file mode 100644 index 0000000000..dfdf07ce39 --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java @@ -0,0 +1,58 @@ +package org.thoughtcrime.securesms.database.loaders; + +import android.content.Context; +import android.support.v4.content.AsyncTaskLoader; +import android.util.Log; + +import org.whispersystems.textsecure.api.TextSecureAccountManager; +import org.whispersystems.textsecure.api.messages.multidevice.DeviceInfo; +import org.whispersystems.textsecure.api.push.TextSecureAddress; + +import java.io.IOException; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +public class DeviceListLoader extends AsyncTaskLoader> { + + private static final String TAG = DeviceListLoader.class.getSimpleName(); + + private final TextSecureAccountManager accountManager; + + public DeviceListLoader(Context context, TextSecureAccountManager accountManager) { + super(context); + this.accountManager = accountManager; + } + + @Override + public List loadInBackground() { + try { + List devices = accountManager.getDevices(); + Iterator iterator = devices.iterator(); + + while (iterator.hasNext()) { + if ((iterator.next().getId() == TextSecureAddress.DEFAULT_DEVICE_ID)) { + iterator.remove(); + } + } + + Collections.sort(devices, new DeviceInfoComparator()); + + return devices; + } catch (IOException e) { + Log.w(TAG, e); + return null; + } + } + + private static class DeviceInfoComparator implements Comparator { + + @Override + public int compare(DeviceInfo lhs, DeviceInfo rhs) { + if (lhs.getCreated() < rhs.getCreated()) return -1; + else if (lhs.getCreated() != rhs.getCreated()) return 1; + else return 0; + } + } +} diff --git a/src/org/thoughtcrime/securesms/dependencies/TextSecureCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/TextSecureCommunicationModule.java index c9a4e77d7a..14e014a27a 100644 --- a/src/org/thoughtcrime/securesms/dependencies/TextSecureCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/TextSecureCommunicationModule.java @@ -3,6 +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.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore; import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; @@ -12,7 +13,6 @@ import org.thoughtcrime.securesms.jobs.DeliveryReceiptJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.PushGroupSendJob; import org.thoughtcrime.securesms.jobs.PushMediaSendJob; -import org.thoughtcrime.securesms.jobs.PushContentReceiveJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; import org.thoughtcrime.securesms.jobs.PushTextSendJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; @@ -39,7 +39,8 @@ import dagger.Provides; RefreshPreKeysJob.class, MessageRetrievalService.class, PushNotificationReceiveJob.class, - MultiDeviceContactUpdateJob.class}) + MultiDeviceContactUpdateJob.class, + DeviceListActivity.DeviceListFragment.class}) public class TextSecureCommunicationModule { private final Context context; diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index a3f1c88ac9..2d2d2dd25f 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -50,6 +50,7 @@ import org.whispersystems.textsecure.api.messages.TextSecureContent; import org.whispersystems.textsecure.api.messages.TextSecureEnvelope; import org.whispersystems.textsecure.api.messages.TextSecureGroup; import org.whispersystems.textsecure.api.messages.TextSecureDataMessage; +import org.whispersystems.textsecure.api.messages.multidevice.RequestMessage; import org.whispersystems.textsecure.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage; import org.whispersystems.textsecure.api.push.TextSecureAddress; @@ -126,7 +127,8 @@ public class PushDecryptJob extends MasterSecretJob { } else if (content.getSyncMessage().isPresent()) { TextSecureSyncMessage syncMessage = content.getSyncMessage().get(); - if (syncMessage.getSent().isPresent()) handleSynchronizeSentMessage(masterSecret, syncMessage.getSent().get(), smsMessageId); + if (syncMessage.getSent().isPresent()) handleSynchronizeSentMessage(masterSecret, syncMessage.getSent().get(), smsMessageId); + else if (syncMessage.getRequest().isPresent()) handleSynchronizeRequestMessage(masterSecret, syncMessage.getRequest().get()); } if (envelope.isPreKeyWhisperMessage()) { @@ -198,6 +200,14 @@ public class PushDecryptJob extends MasterSecretJob { } } + private void handleSynchronizeRequestMessage(MasterSecret masterSecret, RequestMessage message) { + if (message.isContactsRequest()) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new MultiDeviceContactUpdateJob(getContext())); + } + } + private void handleMediaMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, TextSecureDataMessage message, Optional smsMessageId) throws MmsException