After Width: | Height: | Size: 346 B |
After Width: | Height: | Size: 352 B |
After Width: | Height: | Size: 280 B |
After Width: | Height: | Size: 287 B |
After Width: | Height: | Size: 420 B |
After Width: | Height: | Size: 430 B |
After Width: | Height: | Size: 574 B |
After Width: | Height: | Size: 609 B |
After Width: | Height: | Size: 706 B |
After Width: | Height: | Size: 763 B |
@ -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>
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|