Support for device management, limits, and contact requests.
// FREEBIE
|
@ -192,6 +192,10 @@
|
||||||
<activity android:name=".RegistrationProgressActivity"
|
<activity android:name=".RegistrationProgressActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
|
<activity android:name=".DeviceListActivity"
|
||||||
|
android:label="@string/AndroidManifest_manage_paired_devices"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".LogSubmitActivity"
|
<activity android:name=".LogSubmitActivity"
|
||||||
android:label="@string/AndroidManifest__log_submit"
|
android:label="@string/AndroidManifest__log_submit"
|
||||||
android:windowSoftInputMode="stateHidden"
|
android:windowSoftInputMode="stateHidden"
|
||||||
|
|
|
@ -67,7 +67,7 @@ dependencies {
|
||||||
compile 'org.whispersystems:jobmanager:0.11.0'
|
compile 'org.whispersystems:jobmanager:0.11.0'
|
||||||
compile 'org.whispersystems:libpastelog:1.0.6'
|
compile 'org.whispersystems:libpastelog:1.0.6'
|
||||||
compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||||
compile 'org.whispersystems:textsecure-android:1.6.0-RC13'
|
compile 'org.whispersystems:textsecure-android:1.6.0-RC19'
|
||||||
|
|
||||||
compile 'com.squareup.leakcanary:leakcanary-android:1.3.1'
|
compile 'com.squareup.leakcanary:leakcanary-android:1.3.1'
|
||||||
|
|
||||||
|
|
BIN
res/drawable-hdpi/ic_devices_black_48dp.png
Normal file
After Width: | Height: | Size: 346 B |
BIN
res/drawable-hdpi/ic_devices_grey600_48dp.png
Normal file
After Width: | Height: | Size: 352 B |
BIN
res/drawable-mdpi/ic_devices_black_48dp.png
Normal file
After Width: | Height: | Size: 280 B |
BIN
res/drawable-mdpi/ic_devices_grey600_48dp.png
Normal file
After Width: | Height: | Size: 287 B |
BIN
res/drawable-xhdpi/ic_devices_black_48dp.png
Normal file
After Width: | Height: | Size: 420 B |
BIN
res/drawable-xhdpi/ic_devices_grey600_48dp.png
Normal file
After Width: | Height: | Size: 430 B |
BIN
res/drawable-xxhdpi/ic_devices_black_48dp.png
Normal file
After Width: | Height: | Size: 574 B |
BIN
res/drawable-xxhdpi/ic_devices_grey600_48dp.png
Normal file
After Width: | Height: | Size: 609 B |
BIN
res/drawable-xxxhdpi/ic_devices_black_48dp.png
Normal file
After Width: | Height: | Size: 706 B |
BIN
res/drawable-xxxhdpi/ic_devices_grey600_48dp.png
Normal file
After Width: | Height: | Size: 763 B |
39
res/layout/device_list_fragment.xml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<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">
|
||||||
|
|
||||||
|
<LinearLayout android:id="@+id/progress_container"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone" >
|
||||||
|
|
||||||
|
<ProgressBar android:id="@+id/progress"
|
||||||
|
android:indeterminate="true"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" >
|
||||||
|
</ProgressBar>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView android:id="@+id/empty"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center|center_vertical"
|
||||||
|
android:gravity="center|center_vertical"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:text="@string/device_list_fragment__no_devices_paired"/>
|
||||||
|
|
||||||
|
<ListView android:id="@id/android:list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:drawSelectorOnTop="false"/>
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
33
res/layout/device_list_item_view.xml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<org.thoughtcrime.securesms.DeviceListItem xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp">
|
||||||
|
|
||||||
|
<TextView android:id="@+id/name"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textColor="?attr/conversation_list_item_contact_color"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView android:id="@+id/created"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textColor="?attr/conversation_list_item_subject_color"
|
||||||
|
android:fontFamily="sans-serif-light" />
|
||||||
|
|
||||||
|
<TextView android:id="@+id/active"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textColor="?attr/conversation_list_item_subject_color"
|
||||||
|
android:fontFamily="sans-serif-light"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
</org.thoughtcrime.securesms.DeviceListItem>
|
|
@ -122,6 +122,7 @@
|
||||||
<attr name="pref_ic_app_protection" format="reference" />
|
<attr name="pref_ic_app_protection" format="reference" />
|
||||||
<attr name="pref_ic_appearance" format="reference" />
|
<attr name="pref_ic_appearance" format="reference" />
|
||||||
<attr name="pref_ic_storage" format="reference" />
|
<attr name="pref_ic_storage" format="reference" />
|
||||||
|
<attr name="pref_ic_devices" format="reference" />
|
||||||
<attr name="pref_ic_advanced" format="reference" />
|
<attr name="pref_ic_advanced" format="reference" />
|
||||||
|
|
||||||
<attr name="app_protect_timeout_picker_color" format="reference"/>
|
<attr name="app_protect_timeout_picker_color" format="reference"/>
|
||||||
|
|
|
@ -166,6 +166,20 @@
|
||||||
<string name="DateUtils_now">Now</string>
|
<string name="DateUtils_now">Now</string>
|
||||||
<string name="DateUtils_minutes_ago">%d min</string>
|
<string name="DateUtils_minutes_ago">%d min</string>
|
||||||
|
|
||||||
|
<!-- DeviceListActivity -->
|
||||||
|
<string name="DeviceListActivity_disconnect_s">Disconnect \'%s\'?</string>
|
||||||
|
<string name="DeviceListActivity_by_disconnecting_this_device_it_will_no_longer_be_able_to_send_or_receive">By disconnecting this device, it will no longer be able to send or receive messages.</string>
|
||||||
|
<string name="DeviceListActivity_network_connection_failed">Network connection failed...</string>
|
||||||
|
<string name="DeviceListActivity_try_again">Try again</string>
|
||||||
|
<string name="DeviceListActivity_disconnecting_device">Disconnecting device..</string>
|
||||||
|
<string name="DeviceListActivity_disconnecting_device_no_ellipse">Disconnecting device</string>
|
||||||
|
<string name="DeviceListActivity_network_failed">Network failed!</string>
|
||||||
|
|
||||||
|
<!-- DeviceListItem -->
|
||||||
|
<string name="DeviceListItem_unnamed_device">Unnamed device</string>
|
||||||
|
<string name="DeviceListItem_created_s">Created %s</string>
|
||||||
|
<string name="DeviceListItem_last_active_s">Last active %s</string>
|
||||||
|
|
||||||
<!-- ShareActivity -->
|
<!-- ShareActivity -->
|
||||||
<string name="ShareActivity_share_with">Share with</string>
|
<string name="ShareActivity_share_with">Share with</string>
|
||||||
|
|
||||||
|
@ -283,6 +297,7 @@
|
||||||
<string name="DeviceProvisioningActivity_content_progress_no_device">No device found.</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_network_error">Network error.</string>
|
||||||
<string name="DeviceProvisioningActivity_content_progress_key_error">Invalid QR code.</string>
|
<string name="DeviceProvisioningActivity_content_progress_key_error">Invalid QR code.</string>
|
||||||
|
<string name="DeviceProvisioningActivity_sorry_you_have_too_many_devices_registered_already">Sorry, you have too many devices registered already, try removing some...</string>
|
||||||
|
|
||||||
<!-- PassphrasePromptActivity -->
|
<!-- PassphrasePromptActivity -->
|
||||||
<string name="PassphrasePromptActivity_enter_passphrase">Enter passphrase</string>
|
<string name="PassphrasePromptActivity_enter_passphrase">Enter passphrase</string>
|
||||||
|
@ -532,6 +547,9 @@
|
||||||
<string name="country_selection_fragment__loading_countries">Loading countries...</string>
|
<string name="country_selection_fragment__loading_countries">Loading countries...</string>
|
||||||
<string name="country_selection_fragment__search">Search</string>
|
<string name="country_selection_fragment__search">Search</string>
|
||||||
|
|
||||||
|
<!-- device_list_fragment -->
|
||||||
|
<string name="device_list_fragment__no_devices_paired">No devices paired...</string>
|
||||||
|
|
||||||
<!-- log_submit_activity -->
|
<!-- log_submit_activity -->
|
||||||
<string name="log_submit_activity__log_fetch_failed">Could not grab logs from your device. You can still use ADB to get debug logs instead.</string>
|
<string name="log_submit_activity__log_fetch_failed">Could not grab logs from your device. You can still use ADB to get debug logs instead.</string>
|
||||||
<string name="log_submit_activity__thanks">Thanks for your help!</string>
|
<string name="log_submit_activity__thanks">Thanks for your help!</string>
|
||||||
|
@ -715,6 +733,7 @@
|
||||||
<string name="AndroidManifest__media_overview">All images</string>
|
<string name="AndroidManifest__media_overview">All images</string>
|
||||||
<string name="AndroidManifest__media_overview_named">All images with %1$s</string>
|
<string name="AndroidManifest__media_overview_named">All images with %1$s</string>
|
||||||
<string name="AndroidManifest__message_details">Message Details</string>
|
<string name="AndroidManifest__message_details">Message Details</string>
|
||||||
|
<string name="AndroidManifest_manage_paired_devices">Manage paired devices</string>
|
||||||
|
|
||||||
<!-- arrays.xml -->
|
<!-- arrays.xml -->
|
||||||
<string name="arrays__import_export">Import / export</string>
|
<string name="arrays__import_export">Import / export</string>
|
||||||
|
@ -959,6 +978,7 @@
|
||||||
<string name="transport_selection_list_item__transport_icon">Transport icon</string>
|
<string name="transport_selection_list_item__transport_icon">Transport icon</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- EOF -->
|
<!-- EOF -->
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -173,6 +173,7 @@
|
||||||
<item name="pref_ic_app_protection">@drawable/ic_app_protection_black</item>
|
<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_appearance">@drawable/ic_brightness_6_black</item>
|
||||||
<item name="pref_ic_storage">@drawable/ic_delete_black</item>
|
<item name="pref_ic_storage">@drawable/ic_delete_black</item>
|
||||||
|
<item name="pref_ic_devices">@drawable/ic_devices_black_48dp</item>
|
||||||
<item name="pref_ic_advanced">@drawable/ic_advanced_black</item>
|
<item name="pref_ic_advanced">@drawable/ic_advanced_black</item>
|
||||||
|
|
||||||
<item name="app_protect_timeout_picker_color">@style/BetterPickersDialogFragment.Light</item>
|
<item name="app_protect_timeout_picker_color">@style/BetterPickersDialogFragment.Light</item>
|
||||||
|
@ -297,6 +298,7 @@
|
||||||
<item name="pref_ic_app_protection">@drawable/ic_app_protection_gray</item>
|
<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_appearance">@drawable/ic_brightness_6_gray</item>
|
||||||
<item name="pref_ic_storage">@drawable/ic_delete_gray</item>
|
<item name="pref_ic_storage">@drawable/ic_delete_gray</item>
|
||||||
|
<item name="pref_ic_devices">@drawable/ic_devices_grey600_48dp</item>
|
||||||
<item name="pref_ic_advanced">@drawable/ic_advanced_gray</item>
|
<item name="pref_ic_advanced">@drawable/ic_advanced_gray</item>
|
||||||
|
|
||||||
<item name="app_protect_timeout_picker_color">@style/BetterPickersDialogFragment</item>
|
<item name="app_protect_timeout_picker_color">@style/BetterPickersDialogFragment</item>
|
||||||
|
|
|
@ -21,6 +21,12 @@
|
||||||
android:title="@string/preferences__delete_old_messages"
|
android:title="@string/preferences__delete_old_messages"
|
||||||
android:icon="?pref_ic_storage"/>
|
android:icon="?pref_ic_storage"/>
|
||||||
|
|
||||||
|
<Preference android:key="preference_category_devices"
|
||||||
|
android:title="Devices"
|
||||||
|
android:icon="?pref_ic_devices">
|
||||||
|
|
||||||
|
</Preference>
|
||||||
|
|
||||||
<Preference android:key="preference_category_advanced"
|
<Preference android:key="preference_category_advanced"
|
||||||
android:title="@string/preferences__advanced"
|
android:title="@string/preferences__advanced"
|
||||||
android:icon="?pref_ic_advanced"/>
|
android:icon="?pref_ic_advanced"/>
|
||||||
|
|
|
@ -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_APP_PROTECTION = "preference_category_app_protection";
|
||||||
private static final String PREFERENCE_CATEGORY_APPEARANCE = "preference_category_appearance";
|
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_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 static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||||
|
@ -131,6 +132,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_APPEARANCE));
|
.setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_APPEARANCE));
|
||||||
this.findPreference(PREFERENCE_CATEGORY_STORAGE)
|
this.findPreference(PREFERENCE_CATEGORY_STORAGE)
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(masterSecret, 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)
|
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_ADVANCED));
|
.setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_ADVANCED));
|
||||||
}
|
}
|
||||||
|
@ -166,7 +169,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
Fragment fragment;
|
Fragment fragment = null;
|
||||||
|
|
||||||
switch (category) {
|
switch (category) {
|
||||||
case PREFERENCE_CATEGORY_SMS_MMS:
|
case PREFERENCE_CATEGORY_SMS_MMS:
|
||||||
|
@ -184,6 +187,10 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||||
case PREFERENCE_CATEGORY_STORAGE:
|
case PREFERENCE_CATEGORY_STORAGE:
|
||||||
fragment = new StoragePreferenceFragment();
|
fragment = new StoragePreferenceFragment();
|
||||||
break;
|
break;
|
||||||
|
case PREFERENCE_CATEGORY_DEVICES:
|
||||||
|
Intent intent = new Intent(getActivity(), DeviceListActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
break;
|
||||||
case PREFERENCE_CATEGORY_ADVANCED:
|
case PREFERENCE_CATEGORY_ADVANCED:
|
||||||
fragment = new AdvancedPreferenceFragment();
|
fragment = new AdvancedPreferenceFragment();
|
||||||
break;
|
break;
|
||||||
|
@ -191,15 +198,17 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
Bundle args = new Bundle();
|
if (fragment != null) {
|
||||||
args.putParcelable("master_secret", masterSecret);
|
Bundle args = new Bundle();
|
||||||
fragment.setArguments(args);
|
args.putParcelable("master_secret", masterSecret);
|
||||||
|
fragment.setArguments(args);
|
||||||
|
|
||||||
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
|
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
|
||||||
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
|
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
|
||||||
fragmentTransaction.replace(android.R.id.content, fragment);
|
fragmentTransaction.replace(android.R.id.content, fragment);
|
||||||
fragmentTransaction.addToBackStack(null);
|
fragmentTransaction.addToBackStack(null);
|
||||||
fragmentTransaction.commit();
|
fragmentTransaction.commit();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
211
src/org/thoughtcrime/securesms/DeviceListActivity.java
Normal file
|
@ -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<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);
|
||||||
|
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();
|
||||||
|
|
||||||
|
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<Void, Void, Void>(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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
62
src/org/thoughtcrime/securesms/DeviceListItem.java
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -6,13 +6,11 @@ import android.content.DialogInterface.OnDismissListener;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.SpannableString;
|
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.afollestad.materialdialogs.DialogAction;
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog;
|
import com.afollestad.materialdialogs.MaterialDialog;
|
||||||
import com.afollestad.materialdialogs.MaterialDialog.Builder;
|
import com.afollestad.materialdialogs.MaterialDialog.Builder;
|
||||||
import com.afollestad.materialdialogs.MaterialDialog.ButtonCallback;
|
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.libaxolotl.ecc.ECPublicKey;
|
||||||
import org.whispersystems.textsecure.api.TextSecureAccountManager;
|
import org.whispersystems.textsecure.api.TextSecureAccountManager;
|
||||||
import org.whispersystems.textsecure.api.push.exceptions.NotFoundException;
|
import org.whispersystems.textsecure.api.push.exceptions.NotFoundException;
|
||||||
|
import org.whispersystems.textsecure.internal.push.DeviceLimitExceededException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@ -94,10 +93,11 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActiv
|
||||||
R.string.DeviceProvisioningActivity_content_progress_title,
|
R.string.DeviceProvisioningActivity_content_progress_title,
|
||||||
R.string.DeviceProvisioningActivity_content_progress_content)
|
R.string.DeviceProvisioningActivity_content_progress_content)
|
||||||
{
|
{
|
||||||
private static final int SUCCESS = 0;
|
private static final int SUCCESS = 0;
|
||||||
private static final int NO_DEVICE = 1;
|
private static final int NO_DEVICE = 1;
|
||||||
private static final int NETWORK_ERROR = 2;
|
private static final int NETWORK_ERROR = 2;
|
||||||
private static final int KEY_ERROR = 3;
|
private static final int KEY_ERROR = 3;
|
||||||
|
private static final int LIMIT_EXCEEDED = 4;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Integer doInBackground(Void... params) {
|
protected Integer doInBackground(Void... params) {
|
||||||
|
@ -113,9 +113,13 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActiv
|
||||||
accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, verificationCode);
|
accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, verificationCode);
|
||||||
return SUCCESS;
|
return SUCCESS;
|
||||||
|
|
||||||
|
|
||||||
} catch (NotFoundException e) {
|
} catch (NotFoundException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
return NO_DEVICE;
|
return NO_DEVICE;
|
||||||
|
} catch (DeviceLimitExceededException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
return LIMIT_EXCEEDED;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
return NETWORK_ERROR;
|
return NETWORK_ERROR;
|
||||||
|
@ -144,6 +148,9 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActiv
|
||||||
case KEY_ERROR:
|
case KEY_ERROR:
|
||||||
Toast.makeText(context, R.string.DeviceProvisioningActivity_content_progress_key_error, Toast.LENGTH_LONG).show();
|
Toast.makeText(context, R.string.DeviceProvisioningActivity_content_progress_key_error, Toast.LENGTH_LONG).show();
|
||||||
break;
|
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();
|
dialog.dismiss();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<List<DeviceInfo>> {
|
||||||
|
|
||||||
|
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<DeviceInfo> loadInBackground() {
|
||||||
|
try {
|
||||||
|
List<DeviceInfo> devices = accountManager.getDevices();
|
||||||
|
Iterator<DeviceInfo> 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<DeviceInfo> {
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.dependencies;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.BuildConfig;
|
import org.thoughtcrime.securesms.BuildConfig;
|
||||||
|
import org.thoughtcrime.securesms.DeviceListActivity;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
|
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
|
||||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
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.MultiDeviceContactUpdateJob;
|
||||||
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||||
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
||||||
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob;
|
|
||||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||||
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||||
|
@ -39,7 +39,8 @@ import dagger.Provides;
|
||||||
RefreshPreKeysJob.class,
|
RefreshPreKeysJob.class,
|
||||||
MessageRetrievalService.class,
|
MessageRetrievalService.class,
|
||||||
PushNotificationReceiveJob.class,
|
PushNotificationReceiveJob.class,
|
||||||
MultiDeviceContactUpdateJob.class})
|
MultiDeviceContactUpdateJob.class,
|
||||||
|
DeviceListActivity.DeviceListFragment.class})
|
||||||
public class TextSecureCommunicationModule {
|
public class TextSecureCommunicationModule {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
|
@ -50,6 +50,7 @@ import org.whispersystems.textsecure.api.messages.TextSecureContent;
|
||||||
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
|
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
|
||||||
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
|
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
|
||||||
import org.whispersystems.textsecure.api.messages.TextSecureDataMessage;
|
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.SentTranscriptMessage;
|
||||||
import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage;
|
import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage;
|
||||||
import org.whispersystems.textsecure.api.push.TextSecureAddress;
|
import org.whispersystems.textsecure.api.push.TextSecureAddress;
|
||||||
|
@ -126,7 +127,8 @@ public class PushDecryptJob extends MasterSecretJob {
|
||||||
} else if (content.getSyncMessage().isPresent()) {
|
} else if (content.getSyncMessage().isPresent()) {
|
||||||
TextSecureSyncMessage syncMessage = content.getSyncMessage().get();
|
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()) {
|
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,
|
private void handleMediaMessage(MasterSecret masterSecret, TextSecureEnvelope envelope,
|
||||||
TextSecureDataMessage message, Optional<Long> smsMessageId)
|
TextSecureDataMessage message, Optional<Long> smsMessageId)
|
||||||
throws MmsException
|
throws MmsException
|
||||||
|
|