Support for archive actions

Fixes #1141
Closes #4698
// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-11-23 15:07:41 -08:00
parent 796decdb0f
commit 2ab0029d49
62 changed files with 796 additions and 138 deletions

View File

@ -173,6 +173,16 @@
</activity-alias>
<activity android:name=".ConversationListArchiveActivity"
android:label="@string/AndroidManifeset_conversations_archive"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:parentActivityName=".ConversationListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
</activity>
<activity android:name=".ConversationActivity"
android:windowSoftInputMode="stateUnchanged"
android:launchMode="singleTask"

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/textsecure_primary">
<item android:id="@android:id/mask" android:drawable="@android:color/black" />
<item>
<selector>
<item android:drawable="@color/textsecure_primary_alpha33" android:state_selected="true" />
<item android:drawable="@color/conversation_list_item_background_read_light" />
</selector>
</item>
</ripple>

View File

@ -5,6 +5,7 @@
<item>
<selector>
<item android:drawable="@color/textsecure_primary_alpha33" android:state_selected="true" />
<item android:drawable="@color/conversation_list_item_background_read_dark" />
</selector>
</item>
</ripple>

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/textsecure_primary_alpha33" android:state_selected="true" />
<item android:drawable="@color/textsecure_primary_alpha33" android:state_pressed="true" />
<item android:drawable="@color/conversation_list_item_background_read_light" />
</selector>

View File

@ -2,4 +2,5 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/textsecure_primary_alpha33" android:state_selected="true" />
<item android:drawable="@color/textsecure_primary_alpha33" android:state_pressed="true" />
<item android:drawable="@color/conversation_list_item_background_read_dark" />
</selector>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="@color/transparent"/>
<stroke android:width="1dp" android:color="#ffbbbbbb"/>
<corners android:radius="5dp"/>
<padding android:bottom="5dp" android:left="5dp" android:right="5dp" android:top="5dp"/>
</shape>

View File

@ -8,7 +8,6 @@
android:layout_height="?android:attr/listPreferredItemHeight"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@drawable/conversation_list_item_background"
android:paddingLeft="48dp"
android:paddingRight="20dp">
@ -18,7 +17,7 @@
android:layout_height="40dp"
android:foreground="@drawable/contact_photo_background"
android:cropToPadding="true"
tools:src="@color/md_material_blue_600"
tools:src="@color/blue_600"
android:layout_marginRight="10dp"
android:contentDescription="@string/SingleContactSelectionActivity_contact_photo" />

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:fab="http://schemas.android.com/apk/res-auto"
android:layout_width="fill_parent"
@ -27,7 +28,7 @@
</LinearLayout>
<com.melnykov.fab.FloatingActionButton
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -35,9 +36,6 @@
android:layout_margin="16dp"
android:src="@drawable/ic_create_white_24dp"
android:focusable="true"
android:contentDescription="@string/conversation_list_fragment__fab_content_description"
fab:fab_colorNormal="?fab_color"
fab:fab_colorPressed="@color/textsecure_primary_dark"
fab:fab_colorRipple="@color/textsecure_primary_dark" />
android:contentDescription="@string/conversation_list_fragment__fab_content_description"/>
</FrameLayout>
</android.support.design.widget.CoordinatorLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.ConversationListItemAction
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="70dp">
<TextView android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="14sp"
android:textStyle="bold"
tools:text="Archived conversations (2)"/>
</org.thoughtcrime.securesms.ConversationListItemAction>

View File

@ -4,7 +4,6 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:background="@drawable/conversation_list_item_background"
android:layout_height="70dp">
<org.thoughtcrime.securesms.components.AvatarImageView
@ -62,6 +61,7 @@
android:layout_height="wrap_content"
android:layout_below="@id/from"
android:layout_toRightOf="@id/error"
android:layout_toLeftOf="@+id/archived"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?attr/conversation_list_item_subject_color"
android:fontFamily="sans-serif-light"
@ -98,5 +98,19 @@
tools:text="30 mins"
android:singleLine="true"/>
<TextView android:id="@+id/archived"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/date"
android:layout_alignParentRight="true"
android:layout_alignBaseline="@id/subject"
android:layout_marginLeft="5dp"
android:text="@string/conversation_list_item_view__archived"
android:textColor="#ffbbbbbb"
android:background="@drawable/rounded_rectangle"
android:textSize="12sp"
/>
</RelativeLayout>
</org.thoughtcrime.securesms.ConversationListItem>

View File

@ -7,9 +7,6 @@
<item android:title="@string/conversation__menu_view_media"
android:id="@+id/menu_view_media" />
<item android:title="@string/conversation__menu_delete_thread"
android:id="@+id/menu_delete_thread" />
<item android:title="@string/conversation__menu_conversation_settings"
android:id="@+id/menu_conversation_settings"/>

View File

@ -5,10 +5,11 @@
<item android:title="@string/conversation_list_batch__menu_delete_selected"
android:id="@+id/menu_delete_selected"
android:icon="?menu_trash_icon"
app:showAsAction="ifRoom" />
app:showAsAction="always" />
<item android:title="@string/conversation_list_batch__menu_select_all"
android:id="@+id/menu_select_all"
android:icon="?menu_selectall_icon" />
android:icon="?menu_selectall_icon"
app:showAsAction="always"/>
</menu>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/conversation_list_batch__archive_selected"
android:id="@+id/menu_archive_selected"
android:icon="@drawable/ic_archive_white_24dp"
app:showAsAction="always"/>
</menu>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/conversation_list_batch__archive_selected"
android:id="@+id/menu_archive_selected"
android:icon="@drawable/ic_unarchive_white_24dp"
app:showAsAction="always"/>
</menu>

View File

@ -24,7 +24,7 @@
<color name="gray95_transparent50">#7F111111</color>
<color name="conversation_list_item_background_read_light">#ffffffff</color>
<color name="conversation_list_item_background_read_light">@color/gray5</color>
<color name="conversation_list_item_background_unread_light">#ffffffff</color>
<color name="conversation_list_item_background_read_dark">#ff000000</color>
<color name="conversation_list_item_background_unread_dark">#ff333333</color>

View File

@ -37,6 +37,7 @@
<dimen name="transfer_controls_expanded_width">150dp</dimen>
<dimen name="transfer_controls_contracted_width">70dp</dimen>
<dimen name="conversation_list_fragment_archive_padding">16dp</dimen>
<!-- RedPhone -->
<dimen name="incoming_widget_outer_radius">135dip</dimen>

View File

@ -174,10 +174,18 @@
</plurals>
<string name="ConversationListFragment_deleting">Deleting</string>
<string name="ConversationListFragment_deleting_selected_threads">Deleting selected threads...</string>
<string name="ConversationListFragment_archived_conversations">Archived conversations</string>
<string name="ConversationListFragment_undo">UNDO</string>
<string name="ConversationListFragment_moved_conversation_to_inbox">Moved conversation to inbox</string>
<string name="ConversationListFragment_archived_conversation">Archived conversation</string>
<string name="ConversationListFragment_moved_conversations_to_inbox">Moved conversations to inbox</string>
<!-- ConversationListItem -->
<string name="ConversationListItem_key_exchange_message">Key exchange message...</string>
<!-- ConversationListItemAction -->
<string name="ConversationListItemAction_archived_conversations_d">Archived conversations (%d)</string>
<!-- CustomDefaultPreference -->
<string name="CustomDefaultPreference_using_custom">Using custom: %s</string>
<string name="CustomDefaultPreference_using_default">Using default: %s</string>
@ -867,6 +875,7 @@
<string name="AndroidManifest__message_details">Message details</string>
<string name="AndroidManifest_manage_linked_devices">Manage linked devices</string>
<string name="AndroidManifest__invite_friends">Invite friends</string>
<string name="AndroidManifeset_conversations_archive">Conversations archive</string>
<!-- arrays.xml -->
<string name="arrays__import_export">Import / export</string>
@ -1048,6 +1057,7 @@
<!-- conversation_list_batch -->
<string name="conversation_list_batch__menu_delete_selected">Delete selected</string>
<string name="conversation_list_batch__menu_select_all">Select all</string>
<string name="conversation_list_batch__archive_selected">Archive selected</string>
<!-- conversation_list -->
<string name="conversation_list__menu_search">Search</string>
@ -1055,6 +1065,7 @@
<!-- conversation_list_item_view -->
<string name="conversation_list_item_view__contact_photo_image">Contact Photo Image</string>
<string name="conversation_list_item_view__error_alert">Error alert</string>
<string name="conversation_list_item_view__archived">Archived</string>
<!-- conversation_list_fragment -->
<string name="conversation_list_fragment__fab_content_description">New conversation</string>
@ -1151,6 +1162,7 @@
<!-- transport_selection_list_item -->
<string name="transport_selection_list_item__transport_icon">Transport icon</string>
<!-- EOF -->
</resources>

View File

@ -95,7 +95,7 @@
<item name="attachment_type_selector_background">@color/white</item>
<item name="conversation_list_item_background_selected">@drawable/list_selected_holo_light</item>
<item name="conversation_list_item_background_unread">@drawable/conversation_list_item_unread_background</item>
<item name="conversation_list_item_background_read">@drawable/conversation_list_item_background</item>
<item name="conversation_list_item_background_read">@drawable/conversation_list_item_read_background</item>
<item name="conversation_list_item_count_color">#66333333</item>
<item name="conversation_list_item_contact_color">#FF333333</item>
<item name="conversation_list_item_subject_color">#FF444444</item>
@ -205,12 +205,13 @@
<item name="actionBarPopupTheme">@style/ThemeOverlay.AppCompat.Dark</item>
<item name="android:textColor">@color/text_color_dark_theme</item>
<item name="android:textColorSecondary">@color/text_color_secondary_dark_theme</item>
<item name="colorAccent">@color/textsecure_primary_dark</item>
<item name="colorControlActivated">@color/signal_primary_dark</item>
<item name="colorControlHighlight">@color/signal_primary_dark</item>
<item name="android:windowBackground">@color/black</item>
<item name="conversation_list_item_background_selected">@drawable/list_selected_holo_dark</item>
<item name="conversation_list_item_background_unread">@drawable/conversation_list_item_unread_background_dark</item>
<item name="conversation_list_item_background_read">@drawable/conversation_list_item_background</item>
<item name="conversation_list_item_background_read">@drawable/conversation_list_item_read_background_dark</item>
<item name="conversation_list_item_count_color">#66dddddd</item>
<item name="conversation_list_item_contact_color">#ffdddddd</item>
<item name="conversation_list_item_subject_color">#ffdddddd</item>

View File

@ -0,0 +1,15 @@
package org.thoughtcrime.securesms;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import java.util.Locale;
import java.util.Set;
public interface BindableConversationListItem extends Unbindable {
public void bind(@NonNull MasterSecret masterSecret, @NonNull ThreadRecord thread,
@NonNull Locale locale, @NonNull Set<Long> selectedThreads, boolean batchMode);
}

View File

@ -392,7 +392,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
switch (item.getItemId()) {
case R.id.menu_call_secure:
case R.id.menu_call_insecure: handleDial(getRecipients().getPrimaryRecipient()); return true;
case R.id.menu_delete_thread: handleDeleteThread(); return true;
case R.id.menu_add_attachment: handleAddAttachment(); return true;
case R.id.menu_view_media: handleViewMedia(); return true;
case R.id.menu_add_to_contacts: handleAddToContacts(); return true;
@ -650,28 +649,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
new GroupMembersDialog(this, getRecipients()).display();
}
private void handleDeleteThread() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.ConversationActivity_delete_thread_question);
builder.setIconAttribute(R.attr.dialog_alert_icon);
builder.setCancelable(true);
builder.setMessage(R.string.ConversationActivity_this_will_permanently_delete_all_messages_in_this_conversation);
builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (threadId > 0) {
DatabaseFactory.getThreadDatabase(ConversationActivity.this).deleteConversation(threadId);
}
composeText.getText().clear();
threadId = -1;
finish();
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
private void handleAddToContacts() {
final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
intent.putExtra(ContactsContract.Intents.Insert.PHONE, recipients.getPrimaryRecipient().getNumber());
@ -1089,9 +1066,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
draftDatabase.insertDrafts(new MasterCipher(thisMasterSecret), threadId, drafts);
threadDatabase.updateSnippet(threadId, drafts.getSnippet(ConversationActivity.this),
drafts.getUriSnippet(ConversationActivity.this),
System.currentTimeMillis(), Types.BASE_DRAFT_TYPE);
System.currentTimeMillis(), Types.BASE_DRAFT_TYPE, true);
} else if (threadId > 0) {
threadDatabase.update(threadId);
threadDatabase.update(threadId, false);
}
return threadId;

View File

@ -57,7 +57,7 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.thoughtcrime.securesms.util.ViewUtil;

View File

@ -161,15 +161,6 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
@Override
public void onCreateConversation(long threadId, Recipients recipients, int distributionType) {
createConversation(threadId, recipients, distributionType);
}
private void createGroup() {
Intent intent = new Intent(this, GroupCreateActivity.class);
startActivity(intent);
}
private void createConversation(long threadId, Recipients recipients, int distributionType) {
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients.getIds());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
@ -179,6 +170,17 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);
}
@Override
public void onSwitchToArchive() {
Intent intent = new Intent(this, ConversationListArchiveActivity.class);
startActivity(intent);
}
private void createGroup() {
Intent intent = new Intent(this, GroupCreateActivity.class);
startActivity(intent);
}
private void handleDisplaySettings() {
Intent preferencesIntent = new Intent(this, ApplicationPreferencesActivity.class);
startActivity(preferencesIntent);

View File

@ -49,6 +49,9 @@ import java.util.Set;
*/
public class ConversationListAdapter extends CursorRecyclerViewAdapter<ConversationListAdapter.ViewHolder> {
private static final int MESSAGE_TYPE_SWITCH_ARCHIVE = 1;
private static final int MESSAGE_TYPE_THREAD = 2;
private final ThreadDatabase threadDatabase;
private final MasterSecret masterSecret;
private final MasterCipher masterCipher;
@ -61,37 +64,25 @@ public class ConversationListAdapter extends CursorRecyclerViewAdapter<Conversat
private boolean batchMode = false;
protected static class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(final @NonNull ConversationListItem itemView,
final @Nullable ItemClickListener clickListener)
public <V extends View & BindableConversationListItem> ViewHolder(final @NonNull V itemView)
{
super(itemView);
itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
if (clickListener != null) clickListener.onItemClick(itemView);
}
});
itemView.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (clickListener != null) clickListener.onItemLongClick(itemView);
return true;
}
});
}
public ConversationListItem getItem() {
return (ConversationListItem)itemView;
public BindableConversationListItem getItem() {
return (BindableConversationListItem)itemView;
}
}
@Override
public long getItemId(@NonNull Cursor cursor) {
ThreadRecord record = getThreadRecord(cursor);
StringBuilder builder = new StringBuilder(""+record.getThreadId());
StringBuilder builder = new StringBuilder("" + record.getThreadId());
for (long recipientId : record.getRecipients().getIds()) {
builder.append("::").append(recipientId);
}
return Conversions.byteArrayToLong(digest.digest(builder.toString().getBytes()));
}
@ -116,10 +107,51 @@ public class ConversationListAdapter extends CursorRecyclerViewAdapter<Conversat
}
}
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, boolean archived) {
ConversationListItem listItem = (ConversationListItem)viewHolder.itemView;
if (!archived) {
DatabaseFactory.getThreadDatabase(getContext()).archiveConversation(listItem.getThreadId());
} else {
DatabaseFactory.getThreadDatabase(getContext()).unarchiveConversation(listItem.getThreadId());
}
}
@Override
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder((ConversationListItem)inflater.inflate(R.layout.conversation_list_item_view,
parent, false), clickListener);
if (viewType == MESSAGE_TYPE_SWITCH_ARCHIVE) {
ConversationListItemAction action = (ConversationListItemAction)inflater.inflate(R.layout.conversation_list_item_action,
parent, false);
action.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (clickListener != null) clickListener.onSwitchToArchive();
}
});
return new ViewHolder(action);
} else {
final ConversationListItem item = (ConversationListItem)inflater.inflate(R.layout.conversation_list_item_view,
parent, false);
item.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
if (clickListener != null) clickListener.onItemClick(item);
}
});
item.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (clickListener != null) clickListener.onItemLongClick(item);
return true;
}
});
return new ViewHolder(item);
}
}
@Override
@ -129,7 +161,18 @@ public class ConversationListAdapter extends CursorRecyclerViewAdapter<Conversat
@Override
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
viewHolder.getItem().set(masterSecret, getThreadRecord(cursor), locale, batchSet, batchMode);
viewHolder.getItem().bind(masterSecret, getThreadRecord(cursor), locale, batchSet, batchMode);
}
@Override
public int getItemViewType(@NonNull Cursor cursor) {
ThreadRecord threadRecord = getThreadRecord(cursor);
if (threadRecord.getDistributionType() == ThreadDatabase.DistributionTypes.ARCHIVE) {
return MESSAGE_TYPE_SWITCH_ARCHIVE;
} else {
return MESSAGE_TYPE_THREAD;
}
}
private ThreadRecord getThreadRecord(@NonNull Cursor cursor) {
@ -168,5 +211,6 @@ public class ConversationListAdapter extends CursorRecyclerViewAdapter<Conversat
public interface ItemClickListener {
void onItemClick(ConversationListItem item);
void onItemLongClick(ConversationListItem item);
void onSwitchToArchive();
}
}

View File

@ -0,0 +1,71 @@
package org.thoughtcrime.securesms;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.view.MenuItem;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class ConversationListArchiveActivity extends PassphraseRequiredActionBarActivity
implements ConversationListFragment.ConversationSelectedListener
{
private final DynamicTheme dynamicTheme = new DynamicTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this);
}
@Override
protected void onCreate(Bundle icicle, @NonNull MasterSecret masterSecret) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
Bundle bundle = new Bundle();
bundle.putBoolean(ConversationListFragment.ARCHIVE, true);
initFragment(android.R.id.content, new ConversationListFragment(),
masterSecret, dynamicLanguage.getCurrentLocale(), bundle);
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
dynamicLanguage.onResume(this);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case R.id.home: super.onBackPressed(); return true;
}
return false;
}
@Override
public void onCreateConversation(long threadId, Recipients recipients, int distributionType) {
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients.getIds());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
startActivity(intent);
overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);
}
@Override
public void onSwitchToArchive() {
throw new AssertionError();
}
}

View File

@ -1,5 +1,5 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2015 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -20,21 +20,31 @@ import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@ -43,7 +53,6 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import com.melnykov.fab.FloatingActionButton;
import org.thoughtcrime.securesms.ConversationListAdapter.ItemClickListener;
import org.thoughtcrime.securesms.components.reminder.DefaultSmsReminder;
@ -60,8 +69,11 @@ import org.thoughtcrime.securesms.database.loaders.ConversationListLoader;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
@ -69,6 +81,9 @@ import java.util.Set;
public class ConversationListFragment extends Fragment
implements LoaderManager.LoaderCallbacks<Cursor>, ActionMode.Callback, ItemClickListener
{
public static final String ARCHIVE = "archive";
private MasterSecret masterSecret;
private ActionMode actionMode;
private RecyclerView list;
@ -76,27 +91,39 @@ public class ConversationListFragment extends Fragment
private FloatingActionButton fab;
private Locale locale;
private String queryFilter = "";
private boolean archive;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
masterSecret = getArguments().getParcelable("master_secret");
locale = (Locale) getArguments().getSerializable(PassphraseRequiredActionBarActivity.LOCALE_EXTRA);
archive = getArguments().getBoolean(ARCHIVE, false);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
final View view = inflater.inflate(R.layout.conversation_list_fragment, container, false);
reminderView = (ReminderView) view.findViewById(R.id.reminder);
list = (RecyclerView) view.findViewById(R.id.list);
fab = (FloatingActionButton) view.findViewById(R.id.fab);
reminderView = ViewUtil.findById(view, R.id.reminder);
list = ViewUtil.findById(view, R.id.list);
fab = ViewUtil.findById(view, R.id.fab);
if (archive) fab.setVisibility(View.GONE);
else fab.setVisibility(View.VISIBLE);
reminderView.setOnDismissListener(new OnDismissListener() {
@Override public void onDismiss() {
@Override
public void onDismiss() {
updateReminders();
}
});
list.setHasFixedSize(true);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
new ItemTouchHelper(new ArchiveListenerCallback()).attachToRecyclerView(list);
return view;
}
@ -170,6 +197,49 @@ public class ConversationListFragment extends Fragment
getLoaderManager().restartLoader(0, null, this);
}
private void handleArchiveAllSelected() {
final Set<Long> selectedConversations = new HashSet<>(getListAdapter().getBatchSelections());
final boolean archive = this.archive;
String snackBarTitle;
if (archive) snackBarTitle = getString(R.string.ConversationListFragment_moved_conversations_to_inbox);
else snackBarTitle = getString(R.string.ConversationListFragment_archived_conversations);
new SnackbarAsyncTask<Void>(getView(), snackBarTitle,
getString(R.string.ConversationListFragment_undo),
getResources().getColor(R.color.amber_500),
Snackbar.LENGTH_LONG, true)
{
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
if (actionMode != null) {
actionMode.finish();
actionMode = null;
}
}
@Override
protected void executeAction(@Nullable Void parameter) {
for (long threadId : selectedConversations) {
if (!archive) DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId);
else DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId);
}
}
@Override
protected void reverseAction(@Nullable Void parameter) {
for (long threadId : selectedConversations) {
if (!archive) DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId);
else DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId);
}
}
}.execute();
}
private void handleDeleteAllSelected() {
int conversationsCount = getListAdapter().getBatchSelections().size();
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
@ -234,7 +304,7 @@ public class ConversationListFragment extends Fragment
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return new ConversationListLoader(getActivity(), queryFilter);
return new ConversationListLoader(getActivity(), queryFilter, archive);
}
@Override
@ -276,21 +346,30 @@ public class ConversationListFragment extends Fragment
getListAdapter().notifyDataSetChanged();
}
@Override
public void onSwitchToArchive() {
((ConversationSelectedListener)getActivity()).onSwitchToArchive();
}
public interface ConversationSelectedListener {
void onCreateConversation(long threadId, Recipients recipients, int distributionType);
void onSwitchToArchive();
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = getActivity().getMenuInflater();
if (archive) inflater.inflate(R.menu.conversation_list_batch_unarchive, menu);
else inflater.inflate(R.menu.conversation_list_batch_archive, menu);
inflater.inflate(R.menu.conversation_list_batch, menu);
mode.setTitle(R.string.conversation_fragment_cab__batch_selection_mode);
mode.setSubtitle(getString(R.string.conversation_fragment_cab__batch_selection_amount, 1));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getActivity().getWindow()
.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
}
return true;
@ -306,6 +385,7 @@ public class ConversationListFragment extends Fragment
switch (item.getItemId()) {
case R.id.menu_select_all: handleSelectAllThreads(); return true;
case R.id.menu_delete_selected: handleDeleteAllSelected(); return true;
case R.id.menu_archive_selected: handleArchiveAllSelected(); return true;
}
return false;
@ -316,8 +396,7 @@ public class ConversationListFragment extends Fragment
getListAdapter().initializeBatchMode(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
TypedArray color = getActivity().getTheme()
.obtainStyledAttributes(new int[] { android.R.attr.statusBarColor });
TypedArray color = getActivity().getTheme().obtainStyledAttributes(new int[] {android.R.attr.statusBarColor});
getActivity().getWindow().setStatusBarColor(color.getColor(0, Color.BLACK));
color.recycle();
}
@ -325,6 +404,114 @@ public class ConversationListFragment extends Fragment
actionMode = null;
}
private class ArchiveListenerCallback extends ItemTouchHelper.SimpleCallback {
public ArchiveListenerCallback() {
super(0, ItemTouchHelper.RIGHT);
}
@Override
public boolean onMove(RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target)
{
return false;
}
@Override
public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
if (viewHolder.itemView instanceof ConversationListItemAction) {
return 0;
}
if (actionMode != null) {
return 0;
}
return super.getSwipeDirs(recyclerView, viewHolder);
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
final long threadId = ((ConversationListItem)viewHolder.itemView).getThreadId();
if (archive) {
new SnackbarAsyncTask<Long>(getView(),
getString(R.string.ConversationListFragment_moved_conversation_to_inbox),
getString(R.string.ConversationListFragment_undo),
getResources().getColor(R.color.amber_500),
Snackbar.LENGTH_SHORT, false)
{
@Override
protected void executeAction(@Nullable Long parameter) {
DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId);
}
@Override
protected void reverseAction(@Nullable Long parameter) {
DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId);
}
}.execute(threadId);
} else {
new SnackbarAsyncTask<Long>(getView(),
getString(R.string.ConversationListFragment_archived_conversation),
getString(R.string.ConversationListFragment_undo),
getResources().getColor(R.color.amber_500),
Snackbar.LENGTH_SHORT, false)
{
@Override
protected void executeAction(@Nullable Long parameter) {
DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId);
}
@Override
protected void reverseAction(@Nullable Long parameter) {
DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId);
}
}.execute(threadId);
}
}
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder,
float dX, float dY, int actionState,
boolean isCurrentlyActive)
{
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
View itemView = viewHolder.itemView;
Paint p = new Paint();
if (dX > 0) {
Bitmap icon;
if (archive) icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_unarchive_white_36dp);
else icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_archive_white_36dp);
p.setColor(getResources().getColor(R.color.green_500));
c.drawRect((float) itemView.getLeft(), (float) itemView.getTop(), dX,
(float) itemView.getBottom(), p);
c.drawBitmap(icon,
(float) itemView.getLeft() + getResources().getDimension(R.dimen.conversation_list_fragment_archive_padding),
(float) itemView.getTop() + ((float) itemView.getBottom() - (float) itemView.getTop() - icon.getHeight())/2,
p);
}
if (Build.VERSION.SDK_INT >= 11) {
float alpha = 1.0f - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
viewHolder.itemView.setAlpha(alpha);
viewHolder.itemView.setTranslationX(dX);
}
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
}
}
}

View File

@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.ResUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Locale;
import java.util.Set;
@ -53,7 +54,8 @@ import static org.thoughtcrime.securesms.util.SpanUtil.color;
*/
public class ConversationListItem extends RelativeLayout
implements Recipients.RecipientsModifiedListener, Unbindable
implements Recipients.RecipientsModifiedListener,
BindableConversationListItem, Unbindable
{
private final static String TAG = ConversationListItem.class.getSimpleName();
@ -66,6 +68,7 @@ public class ConversationListItem extends RelativeLayout
private TextView subjectView;
private FromTextView fromView;
private TextView dateView;
private TextView archivedView;
private boolean read;
private AvatarImageView contactPhotoImage;
private ThumbnailView thumbnailView;
@ -94,10 +97,11 @@ public class ConversationListItem extends RelativeLayout
this.dateView = (TextView) findViewById(R.id.date);
this.contactPhotoImage = (AvatarImageView) findViewById(R.id.contact_photo_image);
this.thumbnailView = (ThumbnailView) findViewById(R.id.thumbnail);
this.archivedView = ViewUtil.findById(this, R.id.archived);
thumbnailView.setClickable(false);
}
public void set(@NonNull MasterSecret masterSecret, @NonNull ThreadRecord thread,
public void bind(@NonNull MasterSecret masterSecret, @NonNull ThreadRecord thread,
@NonNull Locale locale, @NonNull Set<Long> selectedThreads, boolean batchMode)
{
this.selectedThreads = selectedThreads;
@ -118,6 +122,12 @@ public class ConversationListItem extends RelativeLayout
dateView.setTypeface(read ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
}
if (thread.isArchived()) {
this.archivedView.setVisibility(View.VISIBLE);
} else {
this.archivedView.setVisibility(View.GONE);
}
setThumbnailSnippet(masterSecret, thread);
setBatchState(batchMode);
setBackground(thread);
@ -158,7 +168,7 @@ public class ConversationListItem extends RelativeLayout
this.thumbnailView.setVisibility(View.GONE);
LayoutParams subjectParams = (RelativeLayout.LayoutParams)this.subjectView.getLayoutParams();
subjectParams.addRule(RelativeLayout.LEFT_OF, 0);
subjectParams.addRule(RelativeLayout.LEFT_OF, R.id.archived);
this.subjectView.setLayoutParams(subjectParams);
}
}
@ -187,4 +197,5 @@ public class ConversationListItem extends RelativeLayout
}
});
}
}

View File

@ -0,0 +1,50 @@
package org.thoughtcrime.securesms;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Locale;
import java.util.Set;
public class ConversationListItemAction extends LinearLayout implements BindableConversationListItem {
private TextView description;
public ConversationListItemAction(Context context) {
super(context);
}
public ConversationListItemAction(Context context, AttributeSet attrs) {
super(context, attrs);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public ConversationListItemAction(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
this.description = ViewUtil.findById(this, R.id.description);
}
@Override
public void bind(@NonNull MasterSecret masterSecret, @NonNull ThreadRecord thread, @NonNull Locale locale, @NonNull Set<Long> selectedThreads, boolean batchMode) {
this.description.setText(getContext().getString(R.string.ConversationListItemAction_archived_conversations_d, thread.getCount()));
}
@Override
public void unbind() {
}
}

View File

@ -20,7 +20,7 @@ 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.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libaxolotl.IdentityKeyPair;

View File

@ -22,7 +22,7 @@ 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.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.textsecure.api.TextSecureAccountManager;

View File

@ -16,7 +16,7 @@ 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.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyException;

View File

@ -65,7 +65,7 @@ import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter;
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter.OnRecipientDeletedListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences;

View File

@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;

View File

@ -90,7 +90,7 @@ public class ShareFragment extends ListFragment implements LoaderManager.LoaderC
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return new ConversationListLoader(getActivity(), null);
return new ConversationListLoader(getActivity(), null, false);
}
@Override

View File

@ -68,7 +68,8 @@ public class DatabaseFactory {
private static final int INTRODUCED_DB_OPTIMIZATIONS_VERSION = 21;
private static final int INTRODUCED_INVITE_REMINDERS_VERSION = 22;
private static final int INTRODUCED_CONVERSATION_LIST_THUMBNAILS_VERSION = 23;
private static final int DATABASE_VERSION = 23;
private static final int INTRODUCED_ARCHIVE_VERSION = 24;
private static final int DATABASE_VERSION = 24;
private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object();
@ -778,6 +779,11 @@ public class DatabaseFactory {
db.execSQL("ALTER TABLE thread ADD COLUMN snippet_uri TEXT DEFAULT NULL");
}
if (oldVersion < INTRODUCED_ARCHIVE_VERSION) {
db.execSQL("ALTER TABLE thread ADD COLUMN archived INTEGER DEFAULT 0");
db.execSQL("CREATE INDEX IF NOT EXISTS archived_index ON thread (archived)");
}
db.setTransactionSuccessful();
db.endTransaction();
}

View File

@ -449,7 +449,7 @@ public class MmsDatabase extends MessagingDatabase {
long threadId = getThreadIdForMessage(messageId);
DatabaseFactory.getThreadDatabase(context).update(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
notifyConversationListeners(threadId);
notifyConversationListListeners();
@ -604,7 +604,7 @@ public class MmsDatabase extends MessagingDatabase {
contentValues);
DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
notifyConversationListeners(threadId);
jobManager.add(new TrimThreadJob(context, threadId));
@ -692,7 +692,7 @@ public class MmsDatabase extends MessagingDatabase {
public void markIncomingNotificationReceived(long threadId) {
notifyConversationListeners(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
if (org.thoughtcrime.securesms.util.Util.isDefaultSmsProvider(context)) {
DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
@ -808,7 +808,7 @@ public class MmsDatabase extends MessagingDatabase {
db.endTransaction();
notifyConversationListeners(contentValues.getAsLong(THREAD_ID));
DatabaseFactory.getThreadDatabase(context).update(contentValues.getAsLong(THREAD_ID));
DatabaseFactory.getThreadDatabase(context).update(contentValues.getAsLong(THREAD_ID), true);
}
}
@ -821,7 +821,7 @@ public class MmsDatabase extends MessagingDatabase {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId);
boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false);
notifyConversationListeners(threadId);
return threadDeleted;
}

View File

@ -83,7 +83,7 @@ public class PlaintextBackupImporter {
}
for (long threadId : modifiedThreads) {
threads.update(threadId);
threads.update(threadId, true);
}
Log.w("PlaintextBackupImporter", "Exited loop");

View File

@ -118,7 +118,7 @@ public class SmsDatabase extends MessagingDatabase {
long threadId = getThreadIdForMessage(id);
DatabaseFactory.getThreadDatabase(context).update(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId, false);
notifyConversationListeners(threadId);
notifyConversationListListeners();
}
@ -310,7 +310,7 @@ public class SmsDatabase extends MessagingDatabase {
long threadId = getThreadIdForMessage(messageId);
DatabaseFactory.getThreadDatabase(context).update(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
notifyConversationListeners(threadId);
notifyConversationListListeners();
@ -335,7 +335,7 @@ public class SmsDatabase extends MessagingDatabase {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long newMessageId = db.insert(TABLE_NAME, null, contentValues);
DatabaseFactory.getThreadDatabase(context).update(record.getThreadId());
DatabaseFactory.getThreadDatabase(context).update(record.getThreadId(), true);
notifyConversationListeners(record.getThreadId());
jobManager.add(new TrimThreadJob(context, record.getThreadId()));
@ -372,7 +372,7 @@ public class SmsDatabase extends MessagingDatabase {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long messageId = db.insert(TABLE_NAME, null, values);
DatabaseFactory.getThreadDatabase(context).update(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
notifyConversationListeners(threadId);
jobManager.add(new TrimThreadJob(context, threadId));
@ -450,7 +450,7 @@ public class SmsDatabase extends MessagingDatabase {
DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
}
DatabaseFactory.getThreadDatabase(context).update(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
notifyConversationListeners(threadId);
jobManager.add(new TrimThreadJob(context, threadId));
@ -481,7 +481,7 @@ public class SmsDatabase extends MessagingDatabase {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues);
DatabaseFactory.getThreadDatabase(context).update(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
notifyConversationListeners(threadId);
jobManager.add(new TrimThreadJob(context, threadId));
@ -526,7 +526,7 @@ public class SmsDatabase extends MessagingDatabase {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long threadId = getThreadIdForMessage(messageId);
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId);
boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false);
notifyConversationListeners(threadId);
return threadDeleted;
}

View File

@ -197,7 +197,7 @@ public class SmsMigrator {
}
ourSmsDatabase.endTransaction(transaction);
DatabaseFactory.getThreadDatabase(context).update(ourThreadId);
DatabaseFactory.getThreadDatabase(context).update(ourThreadId, true);
DatabaseFactory.getThreadDatabase(context).notifyConversationListeners(ourThreadId);
} finally {

View File

@ -59,19 +59,21 @@ public class ThreadDatabase extends Database {
public static final String SNIPPET = "snippet";
private static final String SNIPPET_CHARSET = "snippet_cs";
public static final String READ = "read";
private static final String TYPE = "type";
public static final String TYPE = "type";
private static final String ERROR = "error";
public static final String SNIPPET_TYPE = "snippet_type";
private static final String SNIPPET_URI = "snippet_uri";
public static final String SNIPPET_URI = "snippet_uri";
public static final String ARCHIVED = "archived";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
DATE + " INTEGER DEFAULT 0, " + MESSAGE_COUNT + " INTEGER DEFAULT 0, " +
RECIPIENT_IDS + " TEXT, " + SNIPPET + " TEXT, " + SNIPPET_CHARSET + " INTEGER DEFAULT 0, " +
READ + " INTEGER DEFAULT 1, " + TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL);";
SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " + ARCHIVED + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_IDS + ");",
"CREATE INDEX IF NOT EXISTS archived_index ON " + TABLE_NAME + " (" + ARCHIVED + ");",
};
public ThreadDatabase(Context context, SQLiteOpenHelper databaseHelper) {
@ -124,27 +126,36 @@ public class ThreadDatabase extends Database {
return db.insert(TABLE_NAME, null, contentValues);
}
private void updateThread(long threadId, long count, String body, @Nullable Uri attachment, long date, long type)
private void updateThread(long threadId, long count, String body, @Nullable Uri attachment, long date, long type, boolean unarchive)
{
ContentValues contentValues = new ContentValues(4);
ContentValues contentValues = new ContentValues(5);
contentValues.put(DATE, date - date % 1000);
contentValues.put(MESSAGE_COUNT, count);
contentValues.put(SNIPPET, body);
contentValues.put(SNIPPET_URI, attachment == null ? null : attachment.toString());
contentValues.put(SNIPPET_TYPE, type);
if (unarchive) {
contentValues.put(ARCHIVED, 0);
}
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""});
notifyConversationListListeners();
}
public void updateSnippet(long threadId, String snippet, @Nullable Uri attachment, long date, long type) {
public void updateSnippet(long threadId, String snippet, @Nullable Uri attachment, long date, long type, boolean unarchive) {
ContentValues contentValues = new ContentValues(3);
contentValues.put(DATE, date - date % 1000);
contentValues.put(SNIPPET, snippet);
contentValues.put(SNIPPET_TYPE, type);
contentValues.put(SNIPPET_URI, attachment == null ? null : attachment.toString());
if (unarchive) {
contentValues.put(ARCHIVED, 0);
}
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""});
notifyConversationListListeners();
@ -217,7 +228,7 @@ public class ThreadDatabase extends Database {
DatabaseFactory.getSmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, lastTweetDate);
DatabaseFactory.getMmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, lastTweetDate);
update(threadId);
update(threadId, false);
notifyConversationListeners(threadId);
}
} finally {
@ -303,11 +314,59 @@ public class ThreadDatabase extends Database {
public Cursor getConversationList() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, null, null, null, null, null, DATE + " DESC");
Cursor cursor = db.query(TABLE_NAME, null, ARCHIVED + " = ?", new String[] {"0"}, null, null, DATE + " DESC");
setNotifyConverationListListeners(cursor);
return cursor;
}
public Cursor getArchivedConversationList() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, null, ARCHIVED + " = ?", new String[] {"1"}, null, null, DATE + " DESC");
setNotifyConverationListListeners(cursor);
return cursor;
}
public int getArchivedConversationListCount() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = db.query(TABLE_NAME, new String[] {"COUNT(*)"}, ARCHIVED + " = ?",
new String[] {"1"}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getInt(0);
}
} finally {
if (cursor != null) cursor.close();
}
return 0;
}
public void archiveConversation(long threadId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(1);
contentValues.put(ARCHIVED, 1);
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""});
notifyConversationListListeners();
}
public void unarchiveConversation(long threadId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(1);
contentValues.put(ARCHIVED, 0);
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""});
notifyConversationListListeners();
}
public void deleteConversation(long threadId) {
DatabaseFactory.getSmsDatabase(context).deleteThread(threadId);
DatabaseFactory.getMmsDatabase(context).deleteThread(threadId);
@ -317,7 +376,6 @@ public class ThreadDatabase extends Database {
notifyConversationListListeners();
}
public void deleteConversations(Set<Long> selectedConversations) {
DatabaseFactory.getSmsDatabase(context).deleteThreads(selectedConversations);
DatabaseFactory.getMmsDatabase(context).deleteThreads(selectedConversations);
@ -399,7 +457,7 @@ public class ThreadDatabase extends Database {
return null;
}
public boolean update(long threadId) {
public boolean update(long threadId, boolean unarchive) {
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
long count = mmsSmsDatabase.getConversationCount(threadId);
@ -421,7 +479,10 @@ public class ThreadDatabase extends Database {
if (record.isPush()) timestamp = record.getDateSent();
else timestamp = record.getDateReceived();
updateThread(threadId, count, record.getBody().getBody(), getAttachmentUriFor(record), timestamp, record.getType());
updateThread(threadId, count, record.getBody().getBody(),
getAttachmentUriFor(record), timestamp,
record.getType(), unarchive);
notifyConversationListListeners();
return false;
} else {
@ -456,6 +517,7 @@ public class ThreadDatabase extends Database {
public static final int DEFAULT = 2;
public static final int BROADCAST = 1;
public static final int CONVERSATION = 2;
public static final int ARCHIVE = 3;
}
public class Reader {
@ -486,10 +548,11 @@ public class ThreadDatabase extends Database {
long read = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.READ));
long type = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE));
int distributionType = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.TYPE));
boolean archived = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.ARCHIVED)) != 0;
Uri snippetUri = getSnippetUri(cursor);
return new ThreadRecord(context, body, snippetUri, recipients, date, count,
read == 1, threadId, type, distributionType);
read == 1, threadId, type, distributionType, archived);
}
private DisplayRecord.Body getPlaintextBody(Cursor cursor) {

View File

@ -2,30 +2,64 @@ package org.thoughtcrime.securesms.database.loaders;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
import java.util.LinkedList;
import java.util.List;
public class ConversationListLoader extends AbstractCursorLoader {
private final String filter;
private final boolean archived;
public ConversationListLoader(Context context, String filter) {
public ConversationListLoader(Context context, String filter, boolean archived) {
super(context);
this.filter = filter;
this.archived = archived;
}
@Override
public Cursor getCursor() {
if (filter != null && filter.trim().length() != 0) {
List<String> numbers = ContactAccessor.getInstance().getNumbersForThreadSearchFilter(context, filter);
return DatabaseFactory.getThreadDatabase(context).getFilteredConversationList(numbers);
} else {
return DatabaseFactory.getThreadDatabase(context).getConversationList();
if (filter != null && filter.trim().length() != 0) return getFilteredConversationList(filter);
else if (!archived) return getUnarchivedConversationList();
else return getArchivedConversationList();
}
private Cursor getUnarchivedConversationList() {
List<Cursor> cursorList = new LinkedList<>();
cursorList.add(DatabaseFactory.getThreadDatabase(context).getConversationList());
int archivedCount = DatabaseFactory.getThreadDatabase(context)
.getArchivedConversationListCount();
if (archivedCount > 0) {
MatrixCursor switchToArchiveCursor = new MatrixCursor(new String[] {
ThreadDatabase.ID, ThreadDatabase.DATE, ThreadDatabase.MESSAGE_COUNT,
ThreadDatabase.RECIPIENT_IDS, ThreadDatabase.SNIPPET, ThreadDatabase.READ,
ThreadDatabase.TYPE, ThreadDatabase.SNIPPET_TYPE, ThreadDatabase.SNIPPET_URI,
ThreadDatabase.ARCHIVED}, 1);
switchToArchiveCursor.addRow(new Object[] {-1L, System.currentTimeMillis(), archivedCount,
"-1", null, 1, ThreadDatabase.DistributionTypes.ARCHIVE, 0, null, 0});
cursorList.add(switchToArchiveCursor);
}
return new MergeCursor(cursorList.toArray(new Cursor[0]));
}
private Cursor getArchivedConversationList() {
return DatabaseFactory.getThreadDatabase(context).getArchivedConversationList();
}
private Cursor getFilteredConversationList(String filter) {
List<String> numbers = ContactAccessor.getInstance().getNumbersForThreadSearchFilter(context, filter);
return DatabaseFactory.getThreadDatabase(context).getFilteredConversationList(numbers);
}
}

View File

@ -44,10 +44,11 @@ public class ThreadRecord extends DisplayRecord {
private final long count;
private final boolean read;
private final int distributionType;
private final boolean archived;
public ThreadRecord(@NonNull Context context, @NonNull Body body, @Nullable Uri snippetUri,
@NonNull Recipients recipients, long date, long count, boolean read,
long threadId, long snippetType, int distributionType)
long threadId, long snippetType, int distributionType, boolean archived)
{
super(context, body, recipients, date, date, threadId, snippetType);
this.context = context.getApplicationContext();
@ -55,6 +56,7 @@ public class ThreadRecord extends DisplayRecord {
this.count = count;
this.read = read;
this.distributionType = distributionType;
this.archived = archived;
}
public @Nullable Uri getSnippetUri() {
@ -124,6 +126,10 @@ public class ThreadRecord extends DisplayRecord {
return getDateReceived();
}
public boolean isArchived() {
return archived;
}
public int getDistributionType() {
return distributionType;
}

View File

@ -31,7 +31,7 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactIdentityManager;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.push.TextSecureCommunicationFactory;
import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.TextSecureAccountManager;

View File

@ -13,6 +13,7 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import java.io.File;
import java.io.FileOutputStream;

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.util;
package org.thoughtcrime.securesms.util.task;
import android.app.ProgressDialog;
import android.content.Context;
@ -7,6 +7,7 @@ import android.os.AsyncTask;
import java.lang.ref.WeakReference;
public abstract class ProgressDialogAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
private final WeakReference<Context> contextReference;
private ProgressDialog progress;
private final String title;

View File

@ -0,0 +1,94 @@
package org.thoughtcrime.securesms.util.task;
import android.app.ProgressDialog;
import android.os.AsyncTask;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.view.View;
public abstract class SnackbarAsyncTask<Params>
extends AsyncTask<Params, Void, Void>
implements View.OnClickListener
{
private final View view;
private final String snackbarText;
private final String snackbarActionText;
private final int snackbarActionColor;
private final int snackbarDuration;
private final boolean showProgress;
private @Nullable Params reversibleParameter;
private @Nullable ProgressDialog progressDialog;
public SnackbarAsyncTask(View view,
String snackbarText,
String snackbarActionText,
int snackbarActionColor,
int snackbarDuration,
boolean showProgress)
{
this.view = view;
this.snackbarText = snackbarText;
this.snackbarActionText = snackbarActionText;
this.snackbarActionColor = snackbarActionColor;
this.snackbarDuration = snackbarDuration;
this.showProgress = showProgress;
}
@Override
protected void onPreExecute() {
if (this.showProgress) this.progressDialog = ProgressDialog.show(view.getContext(), "", "", true);
else this.progressDialog = null;
}
@SafeVarargs
@Override
protected final Void doInBackground(Params... params) {
this.reversibleParameter = params != null && params.length > 0 ?params[0] : null;
executeAction(reversibleParameter);
return null;
}
@Override
protected void onPostExecute(Void result) {
if (this.showProgress && this.progressDialog != null) {
this.progressDialog.dismiss();
this.progressDialog = null;
}
Snackbar.make(view, snackbarText, snackbarDuration)
.setAction(snackbarActionText, this)
.setActionTextColor(snackbarActionColor)
.show();
}
@Override
public void onClick(View v) {
new AsyncTask<Void, Void, Void>() {
@Override
protected void onPreExecute() {
if (showProgress) progressDialog = ProgressDialog.show(view.getContext(), "", "", true);
else progressDialog = null;
}
@Override
protected Void doInBackground(Void... params) {
reverseAction(reversibleParameter);
return null;
}
@Override
protected void onPostExecute(Void result) {
if (showProgress && progressDialog != null) {
progressDialog.dismiss();
progressDialog = null;
}
}
}.execute();
}
protected abstract void executeAction(@Nullable Params parameter);
protected abstract void reverseAction(@Nullable Params parameter);
}