File indexing completed on 2024-12-22 04:41:40

0001 /*
0002  * SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 package org.kde.kdeconnect.Plugins.NotificationsPlugin;
0008 
0009 import android.app.Activity;
0010 import android.app.KeyguardManager;
0011 import android.app.Notification;
0012 import android.app.PendingIntent;
0013 import android.app.RemoteInput;
0014 import android.content.Context;
0015 import android.content.Intent;
0016 import android.content.SharedPreferences;
0017 import android.content.pm.PackageManager;
0018 import android.content.res.Resources;
0019 import android.graphics.Bitmap;
0020 import android.graphics.Canvas;
0021 import android.graphics.drawable.Drawable;
0022 import android.graphics.drawable.Icon;
0023 import android.os.Build;
0024 import android.os.Bundle;
0025 import android.os.Parcelable;
0026 import android.provider.Settings;
0027 import android.service.notification.StatusBarNotification;
0028 import android.text.SpannableString;
0029 import android.text.TextUtils;
0030 import android.util.Log;
0031 import android.util.Pair;
0032 
0033 import androidx.annotation.NonNull;
0034 import androidx.annotation.Nullable;
0035 import androidx.annotation.RequiresApi;
0036 import androidx.core.app.NotificationCompat;
0037 import androidx.fragment.app.DialogFragment;
0038 
0039 import org.apache.commons.collections4.MultiValuedMap;
0040 import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
0041 import org.apache.commons.lang3.ArrayUtils;
0042 import org.apache.commons.lang3.StringUtils;
0043 import org.json.JSONArray;
0044 import org.kde.kdeconnect.Helpers.AppsHelper;
0045 import org.kde.kdeconnect.NetworkPacket;
0046 import org.kde.kdeconnect.Plugins.Plugin;
0047 import org.kde.kdeconnect.Plugins.PluginFactory;
0048 import org.kde.kdeconnect.UserInterface.MainActivity;
0049 import org.kde.kdeconnect.UserInterface.PluginSettingsFragment;
0050 import org.kde.kdeconnect.UserInterface.StartActivityAlertDialogFragment;
0051 import org.kde.kdeconnect_tp.R;
0052 
0053 import java.io.ByteArrayOutputStream;
0054 import java.security.MessageDigest;
0055 import java.security.NoSuchAlgorithmException;
0056 import java.util.Arrays;
0057 import java.util.HashMap;
0058 import java.util.HashSet;
0059 import java.util.Map;
0060 import java.util.Objects;
0061 import java.util.Set;
0062 
0063 @PluginFactory.LoadablePlugin
0064 public class NotificationsPlugin extends Plugin implements NotificationReceiver.NotificationListener {
0065 
0066     private final static String PACKET_TYPE_NOTIFICATION = "kdeconnect.notification";
0067     private final static String PACKET_TYPE_NOTIFICATION_REQUEST = "kdeconnect.notification.request";
0068     private final static String PACKET_TYPE_NOTIFICATION_REPLY = "kdeconnect.notification.reply";
0069     private final static String PACKET_TYPE_NOTIFICATION_ACTION = "kdeconnect.notification.action";
0070     private final static String PREF_KEY = "prefKey";
0071     protected static final int PREF_NOTIFICATION_SCREEN_OFF = R.string.screen_off_notification_state;
0072 
0073     private final static String TAG = "KDE/NotificationsPlugin";
0074 
0075     private AppDatabase appDatabase;
0076 
0077     private Set<String> currentNotifications;
0078     private Map<String, RepliableNotification> pendingIntents;
0079     private MultiValuedMap<String, Notification.Action> actions;
0080     private boolean serviceReady;
0081     private SharedPreferences sharedPreferences;
0082     private KeyguardManager keyguardManager;
0083 
0084     @Override
0085     public @NonNull String getDisplayName() {
0086         return context.getResources().getString(R.string.pref_plugin_notifications);
0087     }
0088 
0089     @Override
0090     public @NonNull String getDescription() {
0091         return context.getResources().getString(R.string.pref_plugin_notifications_desc);
0092     }
0093 
0094     @Override
0095     public boolean hasSettings() {
0096         return true;
0097     }
0098 
0099     @Override
0100     public PluginSettingsFragment getSettingsFragment(Activity activity) {
0101         Intent intent = new Intent(activity, NotificationFilterActivity.class);
0102         intent.putExtra(PREF_KEY, this.getSharedPreferencesName());
0103         activity.startActivity(intent);
0104         return null;
0105     }
0106 
0107     @Override
0108     public boolean checkRequiredPermissions() {
0109         //Notifications use a different kind of permission, because it was added before the current runtime permissions model
0110         String notificationListenerList = Settings.Secure.getString(context.getContentResolver(), "enabled_notification_listeners");
0111         return StringUtils.contains(notificationListenerList, context.getPackageName());
0112     }
0113 
0114     @Override
0115     public boolean onCreate() {
0116 
0117         pendingIntents = new HashMap<>();
0118         currentNotifications = new HashSet<>();
0119         actions = new ArrayListValuedHashMap<>();
0120 
0121         sharedPreferences = context.getSharedPreferences(getSharedPreferencesName(),Context.MODE_PRIVATE);
0122 
0123         keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
0124 
0125         appDatabase = new AppDatabase(context, true);
0126 
0127         NotificationReceiver.RunCommand(context, service -> {
0128 
0129             service.addListener(NotificationsPlugin.this);
0130 
0131             serviceReady = service.isConnected();
0132         });
0133 
0134         return true;
0135     }
0136 
0137     @Override
0138     public void onDestroy() {
0139 
0140         NotificationReceiver.RunCommand(context, service -> service.removeListener(NotificationsPlugin.this));
0141     }
0142 
0143     @Override
0144     public void onListenerConnected(NotificationReceiver service) {
0145         serviceReady = true;
0146     }
0147 
0148     @Override
0149     public void onNotificationRemoved(StatusBarNotification statusBarNotification) {
0150         if (statusBarNotification == null) {
0151             Log.w(TAG, "onNotificationRemoved: notification is null");
0152             return;
0153         }
0154         String id = getNotificationKeyCompat(statusBarNotification);
0155 
0156         actions.remove(id);
0157 
0158         if (!appDatabase.isEnabled(statusBarNotification.getPackageName())) {
0159             currentNotifications.remove(id);
0160             return;
0161         }
0162 
0163         NetworkPacket np = new NetworkPacket(PACKET_TYPE_NOTIFICATION);
0164         np.set("id", id);
0165         np.set("isCancel", true);
0166         device.sendPacket(np);
0167         currentNotifications.remove(id);
0168     }
0169 
0170     @Override
0171     public void onNotificationPosted(StatusBarNotification statusBarNotification) {
0172         if (sharedPreferences != null && sharedPreferences.getBoolean(context.getString(PREF_NOTIFICATION_SCREEN_OFF),false)){
0173             if (keyguardManager != null && keyguardManager.inKeyguardRestrictedInputMode()){
0174                 sendNotification(statusBarNotification, false);
0175             }
0176         }else {
0177             sendNotification(statusBarNotification, false);
0178         }
0179     }
0180 
0181     // isPreexisting is true for notifications that we are sending in response to a request command
0182     // and that we want to send with the "silent" flag set
0183     private void sendNotification(StatusBarNotification statusBarNotification, boolean isPreexisting) {
0184 
0185         Notification notification = statusBarNotification.getNotification();
0186 
0187         if ((notification.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0
0188                 || (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0
0189                 || (notification.flags & Notification.FLAG_LOCAL_ONLY) != 0
0190                 || (notification.flags & NotificationCompat.FLAG_GROUP_SUMMARY) != 0 //The notification that groups other notifications
0191         )
0192         {
0193             //This is not a notification we want!
0194             return;
0195         }
0196 
0197         if (!appDatabase.isEnabled(statusBarNotification.getPackageName())) {
0198             return;
0199             // we don't want notification from this app
0200         }
0201 
0202         String key = getNotificationKeyCompat(statusBarNotification);
0203         String packageName = statusBarNotification.getPackageName();
0204         String appName = AppsHelper.appNameLookup(context, packageName);
0205 
0206         if ("com.facebook.orca".equals(packageName) &&
0207                 (statusBarNotification.getId() == 10012) &&
0208                 "Messenger".equals(appName) &&
0209                 notification.tickerText == null) {
0210             //HACK: Hide weird Facebook empty "Messenger" notification that is actually not shown in the phone
0211             return;
0212         }
0213 
0214         if ("com.android.systemui".equals(packageName) &&
0215                 "low_battery".equals(statusBarNotification.getTag())) {
0216             //HACK: Android low battery notification are posted again every few seconds. Ignore them, as we already have a battery indicator.
0217             return;
0218         }
0219 
0220         if ("org.kde.kdeconnect_tp".equals(packageName)) {
0221             // Don't send our own notifications
0222             return;
0223         }
0224 
0225         NetworkPacket np = new NetworkPacket(PACKET_TYPE_NOTIFICATION);
0226 
0227         boolean isUpdate = currentNotifications.contains(key);
0228         //If it's an update, the other end should have the icon already: no need to extract it and create the payload again
0229         if (!isUpdate) {
0230 
0231             currentNotifications.add(key);
0232 
0233             Bitmap appIcon = extractIcon(statusBarNotification, notification);
0234 
0235             if (appIcon != null && !appDatabase.getPrivacy(packageName, AppDatabase.PrivacyOptions.BLOCK_IMAGES)) {
0236                 attachIcon(np, appIcon);
0237             }
0238         }
0239 
0240         np.set("actions", extractActions(notification, key));
0241 
0242         np.set("id", key);
0243         np.set("onlyOnce", (notification.flags & NotificationCompat.FLAG_ONLY_ALERT_ONCE) != 0);
0244         np.set("isClearable", statusBarNotification.isClearable());
0245         np.set("appName", StringUtils.defaultString(appName, packageName));
0246         np.set("time", Long.toString(statusBarNotification.getPostTime()));
0247         np.set("silent", isPreexisting);
0248 
0249         if (!appDatabase.getPrivacy(packageName, AppDatabase.PrivacyOptions.BLOCK_CONTENTS)) {
0250             RepliableNotification rn = extractRepliableNotification(statusBarNotification);
0251             if (rn != null) {
0252                 np.set("requestReplyId", rn.id);
0253                 pendingIntents.put(rn.id, rn);
0254             }
0255             np.set("ticker", getTickerText(notification));
0256 
0257             Pair<String, String> conversation = extractConversation(notification);
0258 
0259             if (conversation.first != null) {
0260                 np.set("title", conversation.first);
0261             } else {
0262                 np.set("title", extractStringFromExtra(getExtras(notification), NotificationCompat.EXTRA_TITLE));
0263             }
0264 
0265             np.set("text", extractText(notification, conversation));
0266         }
0267 
0268         device.sendPacket(np);
0269     }
0270 
0271     private String extractText(Notification notification, Pair<String, String> conversation) {
0272 
0273         if (conversation.second != null) {
0274             return conversation.second;
0275         }
0276 
0277         Bundle extras = getExtras(notification);
0278 
0279         if (extras.containsKey(NotificationCompat.EXTRA_BIG_TEXT)) {
0280             return extractStringFromExtra(extras, NotificationCompat.EXTRA_BIG_TEXT);
0281         }
0282 
0283         return extractStringFromExtra(extras, NotificationCompat.EXTRA_TEXT);
0284     }
0285 
0286     @NonNull
0287     private static Bundle getExtras(Notification notification) {
0288         // NotificationCompat.getExtras() is expected to return non-null values for JELLY_BEAN+
0289         return Objects.requireNonNull(NotificationCompat.getExtras(notification));
0290     }
0291 
0292     private void attachIcon(NetworkPacket np, Bitmap appIcon) {
0293         ByteArrayOutputStream outStream = new ByteArrayOutputStream();
0294         appIcon.compress(Bitmap.CompressFormat.PNG, 90, outStream);
0295         byte[] bitmapData = outStream.toByteArray();
0296 
0297         np.setPayload(new NetworkPacket.Payload(bitmapData));
0298         np.set("payloadHash", getChecksum(bitmapData));
0299     }
0300 
0301     @Nullable
0302     private Bitmap extractIcon(StatusBarNotification statusBarNotification, Notification notification) {
0303         try {
0304             Context foreignContext = context.createPackageContext(statusBarNotification.getPackageName(), 0);
0305 
0306             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && notification.getLargeIcon() != null) {
0307                 return iconToBitmap(foreignContext, notification.getLargeIcon());
0308             } else if (notification.largeIcon != null) {
0309                 return notification.largeIcon;
0310             }
0311 
0312             PackageManager pm = context.getPackageManager();
0313             Resources foreignResources = pm.getResourcesForApplication(statusBarNotification.getPackageName());
0314             Drawable foreignIcon = foreignResources.getDrawable(notification.icon); //Might throw Resources.NotFoundException
0315             return drawableToBitmap(foreignIcon);
0316 
0317         } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
0318             Log.e(TAG, "Package not found", e);
0319         }
0320 
0321         return null;
0322     }
0323 
0324     @Nullable
0325     private JSONArray extractActions(Notification notification, String key) {
0326         if (ArrayUtils.isEmpty(notification.actions)) {
0327             return null;
0328         }
0329 
0330         JSONArray jsonArray = new JSONArray();
0331 
0332         for (Notification.Action action : notification.actions) {
0333 
0334             if (null == action.title)
0335                 continue;
0336 
0337             // Check whether it is a reply action. We have special treatment for them
0338             if (ArrayUtils.isNotEmpty(action.getRemoteInputs()))
0339                 continue;
0340 
0341             jsonArray.put(action.title.toString());
0342 
0343             // A list is automatically created if it doesn't already exist.
0344             actions.put(key, action);
0345         }
0346 
0347         return jsonArray;
0348     }
0349 
0350     private Pair<String, String> extractConversation(Notification notification) {
0351         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
0352             return new Pair<>(null, null);
0353 
0354         if (!notification.extras.containsKey(Notification.EXTRA_MESSAGES))
0355             return new Pair<>(null, null);
0356 
0357         Parcelable[] ms = notification.extras.getParcelableArray(Notification.EXTRA_MESSAGES);
0358 
0359         if (ms == null)
0360             return new Pair<>(null, null);
0361 
0362         String title = notification.extras.getString(Notification.EXTRA_CONVERSATION_TITLE);
0363 
0364         boolean isGroupConversation = notification.extras.getBoolean(NotificationCompat.EXTRA_IS_GROUP_CONVERSATION);
0365 
0366         StringBuilder messagesBuilder = new StringBuilder();
0367 
0368         for (Parcelable p : ms) {
0369             Bundle m = (Bundle) p;
0370 
0371             if (isGroupConversation && m.containsKey("sender")) {
0372                 messagesBuilder.append(m.get("sender"));
0373                 messagesBuilder.append(": ");
0374             }
0375 
0376             messagesBuilder.append(extractStringFromExtra(m, "text"));
0377             messagesBuilder.append("\n");
0378         }
0379 
0380         return new Pair<>(title, messagesBuilder.toString());
0381     }
0382 
0383     private Bitmap drawableToBitmap(Drawable drawable) {
0384         if (drawable == null) return null;
0385 
0386         Bitmap res;
0387         if (drawable.getIntrinsicWidth() > 128 || drawable.getIntrinsicHeight() > 128) {
0388             res = Bitmap.createBitmap(96, 96, Bitmap.Config.ARGB_8888);
0389         } else if (drawable.getIntrinsicWidth() <= 64 || drawable.getIntrinsicHeight() <= 64) {
0390             res = Bitmap.createBitmap(96, 96, Bitmap.Config.ARGB_8888);
0391         } else {
0392             res = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
0393         }
0394 
0395         Canvas canvas = new Canvas(res);
0396         drawable.setBounds(0, 0, res.getWidth(), res.getHeight());
0397         drawable.draw(canvas);
0398         return res;
0399     }
0400 
0401     @RequiresApi(Build.VERSION_CODES.M)
0402     private Bitmap iconToBitmap(Context foreignContext, Icon icon) {
0403         if (icon == null) return null;
0404 
0405         return drawableToBitmap(icon.loadDrawable(foreignContext));
0406     }
0407 
0408     private void replyToNotification(String id, String message) {
0409         if (pendingIntents.isEmpty() || !pendingIntents.containsKey(id)) {
0410             Log.e(TAG, "No such notification");
0411             return;
0412         }
0413 
0414         RepliableNotification repliableNotification = pendingIntents.get(id);
0415         if (repliableNotification == null) {
0416             Log.e(TAG, "No such notification");
0417             return;
0418         }
0419         RemoteInput[] remoteInputs = new RemoteInput[repliableNotification.remoteInputs.size()];
0420 
0421         Intent localIntent = new Intent();
0422         localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
0423         Bundle localBundle = new Bundle();
0424         int i = 0;
0425         for (RemoteInput remoteIn : repliableNotification.remoteInputs) {
0426             remoteInputs[i] = remoteIn;
0427             localBundle.putCharSequence(remoteInputs[i].getResultKey(), message);
0428             i++;
0429         }
0430         RemoteInput.addResultsToIntent(remoteInputs, localIntent, localBundle);
0431 
0432         try {
0433             repliableNotification.pendingIntent.send(context, 0, localIntent);
0434         } catch (PendingIntent.CanceledException e) {
0435             Log.e(TAG, "replyToNotification error: " + e.getMessage());
0436         }
0437         pendingIntents.remove(id);
0438     }
0439 
0440     @Nullable
0441     private RepliableNotification extractRepliableNotification(StatusBarNotification statusBarNotification) {
0442 
0443         if (statusBarNotification.getNotification().actions == null) {
0444             return null;
0445         }
0446 
0447         for (Notification.Action act : statusBarNotification.getNotification().actions) {
0448             if (act != null && act.getRemoteInputs() != null) {
0449                 // Is a reply
0450                 RepliableNotification repliableNotification = new RepliableNotification();
0451                 repliableNotification.remoteInputs.addAll(Arrays.asList(act.getRemoteInputs()));
0452                 repliableNotification.pendingIntent = act.actionIntent;
0453                 repliableNotification.packageName = statusBarNotification.getPackageName();
0454                 repliableNotification.tag = statusBarNotification.getTag(); //TODO find how to pass Tag with sending PendingIntent, might fix Hangout problem
0455 
0456                 return repliableNotification;
0457             }
0458         }
0459 
0460         return null;
0461     }
0462 
0463     private static String extractStringFromExtra(Bundle extras, String key) {
0464         Object extra = extras.get(key);
0465         if (extra == null) {
0466             return null;
0467         } else if (extra instanceof String) {
0468             return (String) extra;
0469         } else if (extra instanceof SpannableString) {
0470             return extra.toString();
0471         } else {
0472             Log.e(TAG, "Don't know how to extract text from extra of type: " + extra.getClass().getCanonicalName());
0473             return null;
0474         }
0475     }
0476 
0477     /**
0478      * Returns the ticker text of the notification.
0479      * If device android version is KitKat or newer, the title and text of the notification is used
0480      * instead the ticker text.
0481      */
0482     private String getTickerText(Notification notification) {
0483         String ticker = "";
0484 
0485         try {
0486             Bundle extras = getExtras(notification);
0487             String extraTitle = extractStringFromExtra(extras, NotificationCompat.EXTRA_TITLE);
0488             String extraText = extractStringFromExtra(extras, NotificationCompat.EXTRA_TEXT);
0489 
0490             if (extraTitle != null && !TextUtils.isEmpty(extraText)) {
0491                 ticker = extraTitle + ": " + extraText;
0492             } else if (extraTitle != null) {
0493                 ticker = extraTitle;
0494             } else if (extraText != null) {
0495                 ticker = extraText;
0496             }
0497         } catch (Exception e) {
0498             Log.e(TAG, "problem parsing notification extras for " + notification.tickerText, e);
0499         }
0500 
0501         if (ticker.isEmpty()) {
0502             ticker = (notification.tickerText != null) ? notification.tickerText.toString() : "";
0503         }
0504 
0505         return ticker;
0506     }
0507 
0508     private void sendCurrentNotifications(NotificationReceiver service) {
0509         StatusBarNotification[] notifications = service.getActiveNotifications();
0510         if (notifications != null) { //Can happen only on API 23 and lower
0511             for (StatusBarNotification notification : notifications) {
0512                 sendNotification(notification, true);
0513             }
0514         }
0515     }
0516 
0517     @Override
0518     public boolean onPacketReceived(final NetworkPacket np) {
0519 
0520         if (np.getType().equals(PACKET_TYPE_NOTIFICATION_ACTION)) {
0521 
0522             String key = np.getString("key");
0523             String title = np.getString("action");
0524             PendingIntent intent = null;
0525 
0526             for (Notification.Action a : actions.get(key)) {
0527                 if (a.title.equals(title)) {
0528                     intent = a.actionIntent;
0529                 }
0530             }
0531 
0532             if (intent != null) {
0533                 try {
0534                     intent.send();
0535                 } catch (PendingIntent.CanceledException e) {
0536                     Log.e(TAG, "Triggering action failed", e);
0537                 }
0538             }
0539 
0540         } else if (np.getBoolean("request")) {
0541 
0542             if (serviceReady) {
0543                 NotificationReceiver.RunCommand(context, this::sendCurrentNotifications);
0544             }
0545 
0546         } else if (np.has("cancel")) {
0547             final String dismissedId = np.getString("cancel");
0548             currentNotifications.remove(dismissedId);
0549             NotificationReceiver.RunCommand(context, service -> service.cancelNotification(dismissedId));
0550         } else if (np.has("requestReplyId") && np.has("message")) {
0551             replyToNotification(np.getString("requestReplyId"), np.getString("message"));
0552         }
0553 
0554         return true;
0555     }
0556 
0557     @Override
0558     public @NonNull DialogFragment getPermissionExplanationDialog() {
0559         return new StartActivityAlertDialogFragment.Builder()
0560                 .setTitle(R.string.pref_plugin_notifications)
0561                 .setMessage(R.string.no_permissions)
0562                 .setPositiveButton(R.string.open_settings)
0563                 .setNegativeButton(R.string.cancel)
0564                 .setIntentAction("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")
0565                 .setStartForResult(true)
0566                 .setRequestCode(MainActivity.RESULT_NEEDS_RELOAD)
0567                 .create();
0568     }
0569 
0570     @Override
0571     public @NonNull String[] getSupportedPacketTypes() {
0572         return new String[]{PACKET_TYPE_NOTIFICATION_REQUEST, PACKET_TYPE_NOTIFICATION_REPLY, PACKET_TYPE_NOTIFICATION_ACTION};
0573     }
0574 
0575     @Override
0576     public @NonNull String[] getOutgoingPacketTypes() {
0577         return new String[]{PACKET_TYPE_NOTIFICATION};
0578     }
0579 
0580     private static String getNotificationKeyCompat(StatusBarNotification statusBarNotification) {
0581         String result;
0582         // first check if it's one of our remoteIds
0583         String tag = statusBarNotification.getTag();
0584         if (StringUtils.startsWith(tag, "kdeconnectId:"))
0585             result = Integer.toString(statusBarNotification.getId());
0586         else {
0587             result = statusBarNotification.getKey();
0588         }
0589         return result;
0590     }
0591 
0592     private String getChecksum(byte[] data) {
0593 
0594         try {
0595             MessageDigest md = MessageDigest.getInstance("MD5");
0596             md.update(data);
0597             return bytesToHex(md.digest());
0598         } catch (NoSuchAlgorithmException e) {
0599             Log.e(TAG, "Error while generating checksum", e);
0600         }
0601         return null;
0602     }
0603 
0604 
0605     private static String bytesToHex(byte[] bytes) {
0606         char[] hexArray = "0123456789ABCDEF".toCharArray();
0607         char[] hexChars = new char[bytes.length * 2];
0608         for (int j = 0; j < bytes.length; j++) {
0609             int v = bytes[j] & 0xFF;
0610             hexChars[j * 2] = hexArray[v >>> 4];
0611             hexChars[j * 2 + 1] = hexArray[v & 0x0F];
0612         }
0613         return new String(hexChars).toLowerCase();
0614     }
0615 
0616     public static String getPrefKey(){ return PREF_KEY;}
0617 }