Merge branch 'default-stickers'

Stephen Paul Weber created

* default-stickers:
  Default sticker download offer on first start
  Scan files so they show up under images
  Send SIMS with hashes and filename
  sha-1 is the standard name sha1 is just for bob
  Send cid with known URL without uploading
  Option to download default stickers and save their cid and url in database

Change summary

src/cheogram/AndroidManifest.xml                                         |   1 
src/cheogram/java/com/cheogram/android/BobTransfer.java                  |   9 
src/cheogram/java/com/cheogram/android/DownloadDefaultStickers.java      | 178 
src/main/java/eu/siacs/conversations/entities/Message.java               |  35 
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java    |  24 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java        |  32 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |  13 
src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java       |  44 
src/main/java/eu/siacs/conversations/ui/SettingsActivity.java            |  23 
src/main/java/eu/siacs/conversations/utils/CryptoHelper.java             |   2 
src/main/res/xml/preferences.xml                                         |   3 
11 files changed, 349 insertions(+), 15 deletions(-)

Detailed changes

src/cheogram/AndroidManifest.xml 🔗

@@ -7,6 +7,7 @@
     <application tools:ignore="GoogleAppIndexingWarning">
         <!-- INSERT -->
 
+        <service android:name="com.cheogram.android.DownloadDefaultStickers" />
         <service android:name="com.cheogram.android.ConnectionService"
             android:label="Cheogram"
             android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"

src/cheogram/java/com/cheogram/android/BobTransfer.java 🔗

@@ -13,6 +13,7 @@ import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 
 import io.ipfs.cid.Cid;
+import io.ipfs.multihash.Multihash;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -57,7 +58,13 @@ public class BobTransfer implements Transferable {
 	}
 
 	public static URI uri(Cid cid) throws NoSuchAlgorithmException, URISyntaxException {
-		return new URI("cid", CryptoHelper.multihashAlgo(cid.getType()) + "+" + CryptoHelper.bytesToHex(cid.getHash()) + "@bob.xmpp.org", null);
+		return new URI("cid", multihashAlgo(cid.getType()) + "+" + CryptoHelper.bytesToHex(cid.getHash()) + "@bob.xmpp.org", null);
+	}
+
+	private static String multihashAlgo(Multihash.Type type) throws NoSuchAlgorithmException {
+		final String algo = CryptoHelper.multihashAlgo(type);
+		if (algo.equals("sha-1")) return "sha1";
+		return algo;
 	}
 
 	public BobTransfer(URI uri, Account account, Jid to, XmppConnectionService xmppConnectionService) {

src/cheogram/java/com/cheogram/android/DownloadDefaultStickers.java 🔗

@@ -0,0 +1,178 @@
+package com.cheogram.android;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.Intent;
+import android.database.Cursor;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.IBinder;
+import android.provider.DocumentsContract;
+import android.preference.PreferenceManager;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import androidx.core.app.NotificationCompat;
+
+import com.google.common.io.ByteStreams;
+
+import io.ipfs.cid.Cid;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.utils.Compatibility;
+import eu.siacs.conversations.utils.FileUtils;
+import eu.siacs.conversations.utils.MimeUtils;
+
+public class DownloadDefaultStickers extends Service {
+
+	private static final int NOTIFICATION_ID = 20;
+	private static final AtomicBoolean RUNNING = new AtomicBoolean(false);
+	private DatabaseBackend mDatabaseBackend;
+	private NotificationManager notificationManager;
+	private File mStickerDir;
+	private OkHttpClient http = new OkHttpClient();
+
+	@Override
+	public void onCreate() {
+		mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
+		notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+		mStickerDir = stickerDir();
+	}
+
+	@Override
+	public int onStartCommand(Intent intent, int flags, int startId) {
+		if (RUNNING.compareAndSet(false, true)) {
+			new Thread(() -> {
+				try {
+					download();
+				} catch (final Exception e) {
+					Log.d(Config.LOGTAG, "unable to download stickers", e);
+				}
+				stopForeground(true);
+				RUNNING.set(false);
+				stopSelf();
+			}).start();
+			return START_STICKY;
+		} else {
+			Log.d(Config.LOGTAG, "DownloadDefaultStickers. ignoring start command because already running");
+		}
+		return START_NOT_STICKY;
+	}
+
+	private void oneSticker(JSONObject sticker) throws Exception {
+		Response r = http.newCall(new Request.Builder().url(sticker.getString("url")).build()).execute();
+		File file = new File(mStickerDir.getAbsolutePath() + "/" + sticker.getString("pack") + "/" + sticker.getString("name") + "." + MimeUtils.guessExtensionFromMimeType(r.headers().get("content-type")));
+		file.getParentFile().mkdirs();
+		OutputStream os = new FileOutputStream(file);
+		ByteStreams.copy(r.body().byteStream(), os);
+		os.close();
+
+		JSONArray cids = sticker.getJSONArray("cids");
+		for (int i = 0; i < cids.length(); i++) {
+			Cid cid = Cid.decode(cids.getString(i));
+			mDatabaseBackend.saveCid(cid, file, sticker.getString("url"));
+		}
+
+		MediaScannerConnection.scanFile(
+			getBaseContext(),
+			new String[] { file.getAbsolutePath() },
+			null,
+			new MediaScannerConnection.MediaScannerConnectionClient() {
+				@Override
+				public void onMediaScannerConnected() {}
+
+				@Override
+				public void onScanCompleted(String path, Uri uri) {}
+			}
+		);
+
+		try {
+			File copyright = new File(mStickerDir.getAbsolutePath() + "/" + sticker.getString("pack") + "/copyright.txt");
+			OutputStreamWriter w = new OutputStreamWriter(new FileOutputStream(copyright, true), "utf-8");
+			w.write(sticker.getString("pack"));
+			w.write('/');
+			w.write(sticker.getString("name"));
+			w.write(": ");
+			w.write(sticker.getString("copyright"));
+			w.write('\n');
+			w.close();
+		} catch (final Exception e) { }
+	}
+
+	private void download() throws Exception {
+		NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+		mBuilder.setContentTitle("Downloading Default Stickers")
+				.setSmallIcon(R.drawable.ic_archive_white_24dp)
+				.setProgress(1, 0, false);
+		startForeground(NOTIFICATION_ID, mBuilder.build());
+
+		Response r = http.newCall(new Request.Builder().url("https://stickers.cheogram.com/index.json").build()).execute();
+		JSONArray stickers = new JSONArray(r.body().string());
+
+		final Progress progress = new Progress(mBuilder, 1, 0);
+		for (int i = 0; i < stickers.length(); i++) {
+			oneSticker(stickers.getJSONObject(i));
+
+			final int percentage = i * 100 / stickers.length();
+			notificationManager.notify(NOTIFICATION_ID, progress.build(percentage));
+		}
+	}
+
+	private File stickerDir() {
+		SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
+		final String dir = p.getString("sticker_directory", "Stickers");
+		if (dir.startsWith("content://")) {
+			Uri uri = Uri.parse(dir);
+			uri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri));
+			return new File(FileUtils.getPath(getBaseContext(), uri));
+		} else {
+			return new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/" + dir);
+		}
+	}
+
+	@Override
+	public IBinder onBind(Intent intent) {
+		return null;
+	}
+
+	private static class Progress {
+		private final NotificationCompat.Builder builder;
+		private final int max;
+		private final int count;
+
+		private Progress(NotificationCompat.Builder builder, int max, int count) {
+			this.builder = builder;
+			this.max = max;
+			this.count = count;
+		}
+
+		private Notification build(int percentage) {
+			builder.setProgress(max * 100, count * 100 + percentage, false);
+			return builder.build();
+		}
+	}
+}

src/main/java/eu/siacs/conversations/entities/Message.java 🔗

@@ -1100,6 +1100,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
             fileParams.sims = this.fileParams.sims;
         }
         this.fileParams = fileParams;
+        if (fileParams != null && getSims().isEmpty()) {
+            addPayload(fileParams.toSims());
+        }
     }
 
     public synchronized FileParams getFileParams() {
@@ -1229,6 +1232,21 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
             return file.findChildContent("name", file.getNamespace());
         }
 
+        public void setName(final String name) {
+            if (sims == null) toSims();
+            Element file = getFileElement();
+
+            for (Element child : file.getChildren()) {
+                if (child.getName().equals("name") && child.getNamespace().equals(file.getNamespace())) {
+                    file.removeChild(child);
+                }
+            }
+
+            if (name != null) {
+                file.addChild("name", file.getNamespace()).setContent(name);
+            }
+        }
+
         public Element toSims() {
             if (sims == null) sims = new Element("reference", "urn:xmpp:reference:0");
             sims.setAttribute("type", "data");
@@ -1277,6 +1295,23 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
             return file;
         }
 
+        public void setCids(Iterable<Cid> cids) throws NoSuchAlgorithmException {
+            if (sims == null) toSims();
+            Element file = getFileElement();
+
+            for (Element child : file.getChildren()) {
+                if (child.getName().equals("hash") && child.getNamespace().equals("urn:xmpp:hashes:2")) {
+                    file.removeChild(child);
+                }
+            }
+
+            for (Cid cid : cids) {
+                file.addChild("hash", "urn:xmpp:hashes:2")
+                    .setAttribute("algo", CryptoHelper.multihashAlgo(cid.getType()))
+                    .setContent(Base64.encodeToString(cid.getHash(), Base64.NO_WRAP));
+            }
+        }
+
         public List<Cid> getCids() {
             List<Cid> cids = new ArrayList<>();
             Element file = getFileElement();

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java 🔗

@@ -283,6 +283,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
                 db.execSQL("PRAGMA cheogram.user_version = 6");
             }
 
+            if(cheogramVersion < 7) {
+                db.execSQL(
+                    "ALTER TABLE cheogram.cids " +
+                    "ADD COLUMN url TEXT"
+                );
+                db.execSQL("PRAGMA cheogram.user_version = 7");
+            }
+
             db.setTransactionSuccessful();
         } finally {
             db.endTransaction();
@@ -775,11 +783,27 @@ public class DatabaseBackend extends SQLiteOpenHelper {
         return f;
     }
 
+    public String getUrlForCid(Cid cid) {
+        SQLiteDatabase db = this.getReadableDatabase();
+        Cursor cursor = db.query("cheogram.cids", new String[]{"url"}, "cid=?", new String[]{cid.toString()}, null, null, null);
+        String url = null;
+        if (cursor.moveToNext()) {
+            url = cursor.getString(0);
+        }
+        cursor.close();
+        return url;
+    }
+
     public void saveCid(Cid cid, File file) {
+        saveCid(cid, file, null);
+    }
+
+    public void saveCid(Cid cid, File file, String url) {
         SQLiteDatabase db = this.getWritableDatabase();
         ContentValues cv = new ContentValues();
         cv.put("cid", cid.toString());
         cv.put("path", file.getAbsolutePath());
+        cv.put("url", url);
         db.insertWithOnConflict("cheogram.cids", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
     }
 

src/main/java/eu/siacs/conversations/persistance/FileBackend.java 🔗

@@ -654,6 +654,9 @@ public class FileBackend {
         BitmapFactory.Options options = new BitmapFactory.Options();
         options.inJustDecodeBounds = true;
         try {
+            for (Cid cid : calculateCids(uri)) {
+                if (mXmppConnectionService.getUrlForCid(cid) != null) return true;
+            }
             final InputStream inputStream =
                     mXmppConnectionService.getContentResolver().openInputStream(uri);
             BitmapFactory.decodeStream(inputStream, null, options);
@@ -664,7 +667,7 @@ public class FileBackend {
             return (options.outWidth <= Config.IMAGE_SIZE
                     && options.outHeight <= Config.IMAGE_SIZE
                     && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase()));
-        } catch (FileNotFoundException e) {
+        } catch (final IOException e) {
             Log.d(Config.LOGTAG, "unable to get image dimensions", e);
             return false;
         }
@@ -927,6 +930,10 @@ public class FileBackend {
         }
     }
 
+    public Cid[] calculateCids(final Uri uri) throws IOException {
+        return calculateCids(mXmppConnectionService.getContentResolver().openInputStream(uri));
+    }
+
     public Cid[] calculateCids(final InputStream is) throws IOException {
         try {
             return CryptoHelper.cid(is, new String[]{"SHA-256", "SHA-1", "SHA-512"});
@@ -1727,7 +1734,7 @@ public class FileBackend {
         updateFileParams(message, url, true);
     }
 
-    public void updateFileParams(final Message message, final String url, boolean updateCids) {
+    public void updateFileParams(final Message message, String url, boolean updateCids) {
         final boolean encrypted =
                 message.getEncryption() == Message.ENCRYPTION_PGP
                         || message.getEncryption() == Message.ENCRYPTION_DECRYPTED;
@@ -1739,9 +1746,23 @@ public class FileBackend {
                         || (mime != null && mime.startsWith("image/"));
         Message.FileParams fileParams = message.getFileParams();
         if (fileParams == null) fileParams = new Message.FileParams();
-        if (url != null) {
+        Cid[] cids = new Cid[0];
+        try {
+            cids = calculateCids(new FileInputStream(file));
+            fileParams.setCids(List.of(cids));
+        } catch (final IOException | NoSuchAlgorithmException e) { }
+        if (url == null) {
+            for (Cid cid : cids) {
+                url = mXmppConnectionService.getUrlForCid(cid);
+                if (url != null) {
+                    fileParams.url = url;
+                    break;
+                }
+            }
+        } else {
             fileParams.url = url;
         }
+        fileParams.setName(file.getName());
         if (encrypted && !file.exists()) {
             Log.d(Config.LOGTAG, "skipping updateFileParams because file is encrypted");
             final DownloadableFile encryptedFile = getFile(message, false);
@@ -1801,11 +1822,10 @@ public class FileBackend {
 
         if (updateCids) {
             try {
-                Cid[] cids = calculateCids(new FileInputStream(getFile(message)));
                 for (int i = 0; i < cids.length; i++) {
-                    mXmppConnectionService.saveCid(cids[i], file);
+                    mXmppConnectionService.saveCid(cids[i], file, url);
                 }
-            } catch (final IOException | XmppConnectionService.BlockedMediaException e) { }
+            } catch (XmppConnectionService.BlockedMediaException e) { }
         }
     }
 

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -561,11 +561,19 @@ public class XmppConnectionService extends Service {
         return this.databaseBackend.getFileForCid(cid);
     }
 
+    public String getUrlForCid(Cid cid) {
+        return this.databaseBackend.getUrlForCid(cid);
+    }
+
     public void saveCid(Cid cid, File file) throws BlockedMediaException {
+        saveCid(cid, file, null);
+    }
+
+    public void saveCid(Cid cid, File file, String url) throws BlockedMediaException {
         if (this.databaseBackend.isBlockedMedia(cid)) {
             throw new BlockedMediaException();
         }
-        this.databaseBackend.saveCid(cid, file);
+        this.databaseBackend.saveCid(cid, file, url);
     }
 
     public void blockMedia(File f) {
@@ -1610,6 +1618,9 @@ public class XmppConnectionService extends Service {
 
         final boolean inProgressJoin = isJoinInProgress(conversation);
 
+        if (message.getCounterpart() == null && !message.isPrivateMessage()) {
+            message.setCounterpart(message.getConversation().getJid().asBareJid());
+        }
 
         if (account.isOnlineAndConnected() && !inProgressJoin) {
             switch (message.getEncryption()) {

src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java 🔗

@@ -57,8 +57,11 @@ import androidx.annotation.IdRes;
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.AlertDialog;
+import androidx.core.content.ContextCompat;
 import androidx.databinding.DataBindingUtil;
 
+import com.cheogram.android.DownloadDefaultStickers;
+
 import org.openintents.openpgp.util.OpenPgpApi;
 
 import java.util.Arrays;
@@ -118,6 +121,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     public static final int REQUEST_PLAY_PAUSE = 0x5432;
     public static final int REQUEST_MICROPHONE = 0x5432f;
     public static final int DIALLER_INTEGRATION = 0x5432ff;
+    public static final int REQUEST_DOWNLOAD_STICKERS = 0xbf8702;
 
 
     //secondary fragment (when holding the conversation, must be initialized before refreshing the overview fragment
@@ -217,12 +221,10 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
         }
         final Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
         if (fragment instanceof ConversationsOverviewFragment) {
-            if (ExceptionHelper.checkForCrash(this)) {
-                return;
-            }
-            if (!offerToSetupDiallerIntegration()) {
-                openBatteryOptimizationDialogIfNeeded();
-            }
+            if (ExceptionHelper.checkForCrash(this)) return;
+            if (offerToSetupDiallerIntegration()) return;
+            if (offerToDownloadStickers()) return;
+            openBatteryOptimizationDialogIfNeeded();
         }
     }
 
@@ -259,6 +261,26 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
         }
     }
 
+    private boolean offerToDownloadStickers() {
+        int offered = getPreferences().getInt("default_stickers_offered", 0);
+        if (offered > 0) return false;
+        getPreferences().edit().putInt("default_stickers_offered", 1).apply();
+
+        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        builder.setTitle("Download Stickers?");
+        builder.setMessage("Would you like to download some default sticker packs?");
+        builder.setPositiveButton(R.string.yes, (dialog, which) -> {
+            if (hasStoragePermission(REQUEST_DOWNLOAD_STICKERS)) {
+                downloadStickers();
+            }
+        });
+        builder.setNegativeButton(R.string.no, (dialog, which) -> { });
+        final AlertDialog dialog = builder.create();
+        dialog.setCanceledOnTouchOutside(false);
+        dialog.show();
+        return true;
+    }
+
     private boolean offerToSetupDiallerIntegration() {
         if (mRequestCode == DIALLER_INTEGRATION) {
             mRequestCode = -1;
@@ -353,11 +375,21 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
                             "com.android.server.telecom.settings.EnableAccountPreferenceActivity"));
                         startActivityForResult(intent, DIALLER_INTEGRATION);
                         break;
+                    case REQUEST_DOWNLOAD_STICKERS:
+                        downloadStickers();
+                        break;
                 }
             }
         }
     }
 
+    private void downloadStickers() {
+        Intent intent = new Intent(this, DownloadDefaultStickers.class);
+        ContextCompat.startForegroundService(this, intent);
+        displayToast("Sticker download started");
+        showDialogsIfMainIsOverview();
+    }
+
     @Override
     public void onActivityResult(int requestCode, int resultCode, final Intent data) {
         super.onActivityResult(requestCode, resultCode, data);

src/main/java/eu/siacs/conversations/ui/SettingsActivity.java 🔗

@@ -25,6 +25,8 @@ import androidx.annotation.NonNull;
 import androidx.appcompat.app.AlertDialog;
 import androidx.core.content.ContextCompat;
 
+import com.cheogram.android.DownloadDefaultStickers;
+
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
@@ -69,6 +71,7 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
     public static final String PREVENT_SCREENSHOTS = "prevent_screenshots";
 
     public static final int REQUEST_CREATE_BACKUP = 0xbf8701;
+    public static final int REQUEST_DOWNLOAD_STICKERS = 0xbf8702;
 
     private SettingsFragment mSettingsFragment;
 
@@ -390,6 +393,17 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
             }
         }
 
+        final Preference downloadDefaultStickers = mSettingsFragment.findPreference("download_default_stickers");
+        if (downloadDefaultStickers != null) {
+            downloadDefaultStickers.setOnPreferenceClickListener(
+                    preference -> {
+                        if (hasStoragePermission(REQUEST_DOWNLOAD_STICKERS)) {
+                            downloadStickers();
+                        }
+                        return true;
+                    });
+        }
+
         final Preference clearBlockedMedia = mSettingsFragment.findPreference("clear_blocked_media");
         if (clearBlockedMedia != null) {
             clearBlockedMedia.setOnPreferenceClickListener((p) -> {
@@ -587,6 +601,9 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
                 if (requestCode == REQUEST_CREATE_BACKUP) {
                     createBackup();
                 }
+                if (requestCode == REQUEST_DOWNLOAD_STICKERS) {
+                    downloadStickers();
+                }
             } else {
                 Toast.makeText(
                                 this,
@@ -620,6 +637,12 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
         builder.create().show();
     }
 
+    private void downloadStickers() {
+        Intent intent = new Intent(this, DownloadDefaultStickers.class);
+        ContextCompat.startForegroundService(this, intent);
+        displayToast("Sticker download started");
+    }
+
     private void displayToast(final String msg) {
         runOnUiThread(() -> Toast.makeText(SettingsActivity.this, msg, Toast.LENGTH_LONG).show());
     }

src/main/java/eu/siacs/conversations/utils/CryptoHelper.java 🔗

@@ -293,7 +293,7 @@ public final class CryptoHelper {
     public static String multihashAlgo(Multihash.Type type) throws NoSuchAlgorithmException {
         switch(type) {
         case sha1:
-            return "sha1";
+            return "sha-1";
         case sha2_256:
             return "sha-256";
         case sha2_512:

src/main/res/xml/preferences.xml 🔗

@@ -370,6 +370,9 @@
                 <Preference
                     android:title="Change Stickers Location"
                     android:key="sticker_directory" />
+                <Preference
+                    android:title="Update Default Stickers"
+                    android:key="download_default_stickers" />
                 <Preference
                     android:title="Clear Blocked Media"
                     android:key="clear_blocked_media" />