create share button in backup done notification
|
@ -21,7 +21,9 @@ import java.io.PrintWriter;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.spec.InvalidKeySpecException;
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.zip.GZIPOutputStream;
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
@ -50,6 +52,8 @@ public class ExportBackupService extends Service {
|
||||||
public static final String CIPHERMODE = "AES/GCM/NoPadding";
|
public static final String CIPHERMODE = "AES/GCM/NoPadding";
|
||||||
public static final String PROVIDER = "BC";
|
public static final String PROVIDER = "BC";
|
||||||
|
|
||||||
|
public static final String MIME_TYPE = "application/vnd.conversations.backup";
|
||||||
|
|
||||||
private static final int NOTIFICATION_ID = 19;
|
private static final int NOTIFICATION_ID = 19;
|
||||||
private static final int PAGE_SIZE = 20;
|
private static final int PAGE_SIZE = 20;
|
||||||
private static AtomicBoolean running = new AtomicBoolean(false);
|
private static AtomicBoolean running = new AtomicBoolean(false);
|
||||||
|
@ -213,11 +217,19 @@ public class ExportBackupService extends Service {
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
if (running.compareAndSet(false, true)) {
|
if (running.compareAndSet(false, true)) {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
final boolean success = export();
|
boolean success;
|
||||||
|
List<File> files;
|
||||||
|
try {
|
||||||
|
files = export();
|
||||||
|
success = true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
success = false;
|
||||||
|
files = Collections.emptyList();
|
||||||
|
}
|
||||||
stopForeground(true);
|
stopForeground(true);
|
||||||
running.set(false);
|
running.set(false);
|
||||||
if (success) {
|
if (success) {
|
||||||
notifySuccess();
|
notifySuccess(files);
|
||||||
}
|
}
|
||||||
stopSelf();
|
stopSelf();
|
||||||
}).start();
|
}).start();
|
||||||
|
@ -250,81 +262,97 @@ public class ExportBackupService extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean export() {
|
private List<File> export() throws Exception {
|
||||||
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
|
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
|
||||||
mBuilder.setContentTitle(getString(R.string.notification_create_backup_title))
|
mBuilder.setContentTitle(getString(R.string.notification_create_backup_title))
|
||||||
.setSmallIcon(R.drawable.ic_archive_white_24dp)
|
.setSmallIcon(R.drawable.ic_archive_white_24dp)
|
||||||
.setProgress(1, 0, false);
|
.setProgress(1, 0, false);
|
||||||
startForeground(NOTIFICATION_ID, mBuilder.build());
|
startForeground(NOTIFICATION_ID, mBuilder.build());
|
||||||
try {
|
int count = 0;
|
||||||
int count = 0;
|
final int max = this.mAccounts.size();
|
||||||
final int max = this.mAccounts.size();
|
final SecureRandom secureRandom = new SecureRandom();
|
||||||
final SecureRandom secureRandom = new SecureRandom();
|
final List<File> files = new ArrayList<>();
|
||||||
for (Account account : this.mAccounts) {
|
for (Account account : this.mAccounts) {
|
||||||
final byte[] IV = new byte[12];
|
final byte[] IV = new byte[12];
|
||||||
final byte[] salt = new byte[16];
|
final byte[] salt = new byte[16];
|
||||||
secureRandom.nextBytes(IV);
|
secureRandom.nextBytes(IV);
|
||||||
secureRandom.nextBytes(salt);
|
secureRandom.nextBytes(salt);
|
||||||
final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt);
|
final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt);
|
||||||
final Progress progress = new Progress(mBuilder, max, count);
|
final Progress progress = new Progress(mBuilder, max, count);
|
||||||
final File file = new File(FileBackend.getBackupDirectory(this) + account.getJid().asBareJid().toEscapedString() + ".ceb");
|
final File file = new File(FileBackend.getBackupDirectory(this) + account.getJid().asBareJid().toEscapedString() + ".ceb");
|
||||||
if (file.getParentFile().mkdirs()) {
|
files.add(file);
|
||||||
Log.d(Config.LOGTAG, "created backup directory " + file.getParentFile().getAbsolutePath());
|
if (file.getParentFile().mkdirs()) {
|
||||||
}
|
Log.d(Config.LOGTAG, "created backup directory " + file.getParentFile().getAbsolutePath());
|
||||||
final FileOutputStream fileOutputStream = new FileOutputStream(file);
|
|
||||||
final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
|
|
||||||
backupFileHeader.write(dataOutputStream);
|
|
||||||
dataOutputStream.flush();
|
|
||||||
|
|
||||||
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
|
|
||||||
byte[] key = getKey(account.getPassword(), salt);
|
|
||||||
Log.d(Config.LOGTAG, backupFileHeader.toString());
|
|
||||||
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
|
|
||||||
IvParameterSpec ivSpec = new IvParameterSpec(IV);
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
|
||||||
CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
|
|
||||||
|
|
||||||
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
|
|
||||||
PrintWriter writer = new PrintWriter(gzipOutputStream);
|
|
||||||
SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
|
|
||||||
final String uuid = account.getUuid();
|
|
||||||
accountExport(db, uuid, writer);
|
|
||||||
simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer);
|
|
||||||
messageExport(db, uuid, writer, progress);
|
|
||||||
for (String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
|
|
||||||
simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer);
|
|
||||||
}
|
|
||||||
writer.flush();
|
|
||||||
writer.close();
|
|
||||||
Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
|
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
return true;
|
final FileOutputStream fileOutputStream = new FileOutputStream(file);
|
||||||
} catch (Exception e) {
|
final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
|
||||||
Log.d(Config.LOGTAG, "unable to create backup ", e);
|
backupFileHeader.write(dataOutputStream);
|
||||||
return false;
|
dataOutputStream.flush();
|
||||||
|
|
||||||
|
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
|
||||||
|
byte[] key = getKey(account.getPassword(), salt);
|
||||||
|
Log.d(Config.LOGTAG, backupFileHeader.toString());
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
|
||||||
|
IvParameterSpec ivSpec = new IvParameterSpec(IV);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
||||||
|
CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
|
||||||
|
|
||||||
|
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
|
||||||
|
PrintWriter writer = new PrintWriter(gzipOutputStream);
|
||||||
|
SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
|
||||||
|
final String uuid = account.getUuid();
|
||||||
|
accountExport(db, uuid, writer);
|
||||||
|
simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer);
|
||||||
|
messageExport(db, uuid, writer, progress);
|
||||||
|
for (String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
|
||||||
|
simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer);
|
||||||
|
}
|
||||||
|
writer.flush();
|
||||||
|
writer.close();
|
||||||
|
Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
|
||||||
|
count++;
|
||||||
}
|
}
|
||||||
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifySuccess() {
|
private void notifySuccess(List<File> files) {
|
||||||
final String path = FileBackend.getBackupDirectory(this);
|
final String path = FileBackend.getBackupDirectory(this);
|
||||||
|
|
||||||
PendingIntent pendingIntent = null;
|
PendingIntent openFolderIntent = null;
|
||||||
|
|
||||||
for (Intent intent : getPossibleFileOpenIntents(this, path)) {
|
for (Intent intent : getPossibleFileOpenIntents(this, path)) {
|
||||||
if (intent.resolveActivityInfo(getPackageManager(), 0) != null) {
|
if (intent.resolveActivityInfo(getPackageManager(), 0) != null) {
|
||||||
pendingIntent = PendingIntent.getActivity(this, 189, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
openFolderIntent = PendingIntent.getActivity(this, 189, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PendingIntent shareFilesIntent = null;
|
||||||
|
if (files.size() > 0) {
|
||||||
|
final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
|
||||||
|
ArrayList<Uri> uris = new ArrayList<>();
|
||||||
|
for(File file : files) {
|
||||||
|
uris.add(FileBackend.getUriForFile(this, file));
|
||||||
|
}
|
||||||
|
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
intent.setType(MIME_TYPE);
|
||||||
|
final Intent chooser = Intent.createChooser(intent, getString(R.string.share_backup_files));
|
||||||
|
shareFilesIntent = PendingIntent.getActivity(this,190, chooser, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
}
|
||||||
|
|
||||||
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
|
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
|
||||||
mBuilder.setContentTitle(getString(R.string.notification_backup_created_title))
|
mBuilder.setContentTitle(getString(R.string.notification_backup_created_title))
|
||||||
.setContentText(getString(R.string.notification_backup_created_subtitle, path))
|
.setContentText(getString(R.string.notification_backup_created_subtitle, path))
|
||||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_created_subtitle, FileBackend.getBackupDirectory(this))))
|
.setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_created_subtitle, FileBackend.getBackupDirectory(this))))
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(openFolderIntent)
|
||||||
.setSmallIcon(R.drawable.ic_archive_white_24dp);
|
.setSmallIcon(R.drawable.ic_archive_white_24dp);
|
||||||
|
|
||||||
|
if (shareFilesIntent != null) {
|
||||||
|
mBuilder.addAction(R.drawable.ic_share_white_24dp, getString(R.string.share_backup_files), shareFilesIntent);
|
||||||
|
}
|
||||||
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
|
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,7 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
|
||||||
imageResource = activity.getThemeResource(R.attr.ic_attach_location, R.drawable.ic_attach_location);
|
imageResource = activity.getThemeResource(R.attr.ic_attach_location, R.drawable.ic_attach_location);
|
||||||
showPreviewText = false;
|
showPreviewText = false;
|
||||||
} else {
|
} else {
|
||||||
|
//TODO move this into static MediaPreview method and use same icons as in MediaAdapter
|
||||||
final String mime = message.getMimeType();
|
final String mime = message.getMimeType();
|
||||||
switch (mime == null ? "" : mime.split("/")[0]) {
|
switch (mime == null ? "" : mime.split("/")[0]) {
|
||||||
case "image":
|
case "image":
|
||||||
|
|
|
@ -23,6 +23,7 @@ import java.util.concurrent.RejectedExecutionException;
|
||||||
|
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.databinding.MediaBinding;
|
import eu.siacs.conversations.databinding.MediaBinding;
|
||||||
|
import eu.siacs.conversations.services.ExportBackupService;
|
||||||
import eu.siacs.conversations.ui.XmppActivity;
|
import eu.siacs.conversations.ui.XmppActivity;
|
||||||
import eu.siacs.conversations.ui.util.Attachment;
|
import eu.siacs.conversations.ui.util.Attachment;
|
||||||
import eu.siacs.conversations.ui.util.StyledAttributes;
|
import eu.siacs.conversations.ui.util.StyledAttributes;
|
||||||
|
@ -79,6 +80,8 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
|
||||||
attr = R.attr.media_preview_archive;
|
attr = R.attr.media_preview_archive;
|
||||||
} else if (mime.equals("application/epub+zip") || mime.equals("application/vnd.amazon.mobi8-ebook")) {
|
} else if (mime.equals("application/epub+zip") || mime.equals("application/vnd.amazon.mobi8-ebook")) {
|
||||||
attr = R.attr.media_preview_ebook;
|
attr = R.attr.media_preview_ebook;
|
||||||
|
} else if (mime.equals(ExportBackupService.MIME_TYPE)) {
|
||||||
|
attr = R.attr.media_preview_backup;
|
||||||
} else if (DOCUMENT_MIMES.contains(mime)) {
|
} else if (DOCUMENT_MIMES.contains(mime)) {
|
||||||
attr = R.attr.media_preview_document;
|
attr = R.attr.media_preview_document;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -29,6 +29,7 @@ import java.util.Properties;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.entities.Transferable;
|
import eu.siacs.conversations.entities.Transferable;
|
||||||
|
import eu.siacs.conversations.services.ExportBackupService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utilities for dealing with MIME types.
|
* Utilities for dealing with MIME types.
|
||||||
|
@ -72,6 +73,7 @@ public final class MimeUtils {
|
||||||
add("application/vnd.amazon.mobi8-ebook","kfx");
|
add("application/vnd.amazon.mobi8-ebook","kfx");
|
||||||
add("application/vnd.android.package-archive", "apk");
|
add("application/vnd.android.package-archive", "apk");
|
||||||
add("application/vnd.cinderella", "cdy");
|
add("application/vnd.cinderella", "cdy");
|
||||||
|
add(ExportBackupService.MIME_TYPE, "ceb");
|
||||||
add("application/vnd.ms-pki.stl", "stl");
|
add("application/vnd.ms-pki.stl", "stl");
|
||||||
add("application/vnd.oasis.opendocument.database", "odb");
|
add("application/vnd.oasis.opendocument.database", "odb");
|
||||||
add("application/vnd.oasis.opendocument.formula", "odf");
|
add("application/vnd.oasis.opendocument.formula", "odf");
|
||||||
|
|
|
@ -32,6 +32,7 @@ import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.entities.MucOptions;
|
import eu.siacs.conversations.entities.MucOptions;
|
||||||
import eu.siacs.conversations.entities.Presence;
|
import eu.siacs.conversations.entities.Presence;
|
||||||
import eu.siacs.conversations.entities.Transferable;
|
import eu.siacs.conversations.entities.Transferable;
|
||||||
|
import eu.siacs.conversations.services.ExportBackupService;
|
||||||
import rocks.xmpp.addr.Jid;
|
import rocks.xmpp.addr.Jid;
|
||||||
|
|
||||||
public class UIHelper {
|
public class UIHelper {
|
||||||
|
@ -483,8 +484,12 @@ public class UIHelper {
|
||||||
return context.getString(R.string.pdf_document);
|
return context.getString(R.string.pdf_document);
|
||||||
} else if (mime.equals("application/vnd.android.package-archive")) {
|
} else if (mime.equals("application/vnd.android.package-archive")) {
|
||||||
return context.getString(R.string.apk);
|
return context.getString(R.string.apk);
|
||||||
|
} else if (mime.equals(ExportBackupService.MIME_TYPE)) {
|
||||||
|
return context.getString(R.string.conversations_backup);
|
||||||
} else if (mime.contains("vcard")) {
|
} else if (mime.contains("vcard")) {
|
||||||
return context.getString(R.string.vcard);
|
return context.getString(R.string.vcard);
|
||||||
|
} else if (mime.equals("text/x-vcalendar") || mime.equals("text/calendar")) {
|
||||||
|
return context.getString(R.string.event);
|
||||||
} else if (mime.equals("application/epub+zip") || mime.equals("application/vnd.amazon.mobi8-ebook")) {
|
} else if (mime.equals("application/epub+zip") || mime.equals("application/vnd.amazon.mobi8-ebook")) {
|
||||||
return context.getString(R.string.ebook);
|
return context.getString(R.string.ebook);
|
||||||
} else {
|
} else {
|
||||||
|
|
After Width: | Height: | Size: 561 B |
After Width: | Height: | Size: 589 B |
After Width: | Height: | Size: 386 B |
After Width: | Height: | Size: 405 B |
After Width: | Height: | Size: 733 B |
After Width: | Height: | Size: 770 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.5 KiB |
|
@ -63,6 +63,7 @@
|
||||||
<attr name="media_preview_calendar" format="reference"/>
|
<attr name="media_preview_calendar" format="reference"/>
|
||||||
<attr name="media_preview_archive" format="reference" />
|
<attr name="media_preview_archive" format="reference" />
|
||||||
<attr name="media_preview_ebook" format="reference"/>
|
<attr name="media_preview_ebook" format="reference"/>
|
||||||
|
<attr name="media_preview_backup" format="reference"/>
|
||||||
<attr name="media_preview_unknown" format="reference" />
|
<attr name="media_preview_unknown" format="reference" />
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -868,4 +868,7 @@
|
||||||
<string name="this_looks_like_a_domain">This looks like a domain address</string>
|
<string name="this_looks_like_a_domain">This looks like a domain address</string>
|
||||||
<string name="add_anway">Add anyway</string>
|
<string name="add_anway">Add anyway</string>
|
||||||
<string name="this_looks_like_channel">This looks like a channel address</string>
|
<string name="this_looks_like_channel">This looks like a channel address</string>
|
||||||
|
<string name="share_backup_files">Share backup files</string>
|
||||||
|
<string name="conversations_backup">Conversations backup</string>
|
||||||
|
<string name="event">Event</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
<item type="reference" name="media_preview_calendar">@drawable/ic_event_black_48dp</item>
|
<item type="reference" name="media_preview_calendar">@drawable/ic_event_black_48dp</item>
|
||||||
<item type="reference" name="media_preview_archive">@drawable/ic_archive_black_48dp</item>
|
<item type="reference" name="media_preview_archive">@drawable/ic_archive_black_48dp</item>
|
||||||
<item type="reference" name="media_preview_ebook">@drawable/ic_book_black_48dp</item>
|
<item type="reference" name="media_preview_ebook">@drawable/ic_book_black_48dp</item>
|
||||||
|
<item type="reference" name="media_preview_backup">@drawable/ic_backup_black_48dp</item>
|
||||||
<item type="reference" name="media_preview_unknown">@drawable/ic_help_black_48dp</item>
|
<item type="reference" name="media_preview_unknown">@drawable/ic_help_black_48dp</item>
|
||||||
|
|
||||||
<item type="reference" name="icon_add_group">@drawable/ic_group_add_white_24dp</item>
|
<item type="reference" name="icon_add_group">@drawable/ic_group_add_white_24dp</item>
|
||||||
|
@ -187,6 +188,7 @@
|
||||||
<item type="reference" name="media_preview_calendar">@drawable/ic_event_white_48dp</item>
|
<item type="reference" name="media_preview_calendar">@drawable/ic_event_white_48dp</item>
|
||||||
<item type="reference" name="media_preview_archive">@drawable/ic_archive_white_48dp</item>
|
<item type="reference" name="media_preview_archive">@drawable/ic_archive_white_48dp</item>
|
||||||
<item type="reference" name="media_preview_ebook">@drawable/ic_book_white_48dp</item>
|
<item type="reference" name="media_preview_ebook">@drawable/ic_book_white_48dp</item>
|
||||||
|
<item type="reference" name="media_preview_backup">@drawable/ic_backup_white_48dp</item>
|
||||||
<item type="reference" name="media_preview_unknown">@drawable/ic_help_white_48dp</item>
|
<item type="reference" name="media_preview_unknown">@drawable/ic_help_white_48dp</item>
|
||||||
|
|
||||||
<item type="reference" name="icon_add_group">@drawable/ic_group_add_white_24dp</item>
|
<item type="reference" name="icon_add_group">@drawable/ic_group_add_white_24dp</item>
|
||||||
|
|