diff options
author | Bilal Alsharifi <599206+bilal-alsharifi@users.noreply.github.com> | 2020-04-09 11:54:21 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-04-09 11:54:21 -0400 |
commit | eba22e7520e6acacae4a2a8c7de560167f9cfd05 (patch) | |
tree | a231360ac86180ae8b31e6684f218d79c438632c | |
parent | 57989dd28cdb854596f67792797f60bb5f7982ac (diff) | |
parent | 2714061ca222d0282437ac2fc9a22c77da0e4ba8 (diff) | |
download | sdl_android-eba22e7520e6acacae4a2a8c7de560167f9cfd05.tar.gz |
Merge pull request #1319 from smartdevicelink/bugfix/issue_1316
Cache Lock Screen Icons Retrieved from URL
5 files changed, 349 insertions, 14 deletions
diff --git a/.gitignore b/.gitignore index 13eed80b8..20ec844f5 100644 --- a/.gitignore +++ b/.gitignore @@ -73,4 +73,6 @@ build/ /.idea/libraries /captures .externalNativeBuild - +gradle/ +gradlew +gradlew.bat diff --git a/android/sdl_android/src/androidTest/java/com/smartdevicelink/managers/SdlManagerTests.java b/android/sdl_android/src/androidTest/java/com/smartdevicelink/managers/SdlManagerTests.java index 5c6656bf9..63917067e 100644 --- a/android/sdl_android/src/androidTest/java/com/smartdevicelink/managers/SdlManagerTests.java +++ b/android/sdl_android/src/androidTest/java/com/smartdevicelink/managers/SdlManagerTests.java @@ -25,6 +25,7 @@ import com.smartdevicelink.test.Test; import com.smartdevicelink.transport.BaseTransportConfig; import com.smartdevicelink.transport.TCPTransportConfig; +import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -60,6 +61,8 @@ public class SdlManagerTests extends AndroidTestCase2 { public void setUp() throws Exception{ super.setUp(); + mTestContext = Mockito.mock(Context.class); + // set transport transport = new TCPTransportConfig(TCP_PORT, DEV_MACHINE_IP_ADDRESS, true); @@ -125,6 +128,7 @@ public class SdlManagerTests extends AndroidTestCase2 { builder.setLockScreenConfig(lockScreenConfig); builder.setMinimumProtocolVersion(Test.GENERAL_VERSION); builder.setMinimumRPCVersion(Test.GENERAL_VERSION); + builder.setContext(mTestContext); manager = builder.build(); // mock SdlProxyBase and set it manually diff --git a/android/sdl_android/src/androidTest/java/com/smartdevicelink/managers/lockscreen/LockScreenDeviceIconManagerTests.java b/android/sdl_android/src/androidTest/java/com/smartdevicelink/managers/lockscreen/LockScreenDeviceIconManagerTests.java new file mode 100644 index 000000000..796f900d3 --- /dev/null +++ b/android/sdl_android/src/androidTest/java/com/smartdevicelink/managers/lockscreen/LockScreenDeviceIconManagerTests.java @@ -0,0 +1,105 @@ +package com.smartdevicelink.managers.lockscreen; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Bitmap; + +import com.smartdevicelink.AndroidTestCase2; +import com.smartdevicelink.util.AndroidTools; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; + +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class LockScreenDeviceIconManagerTests extends AndroidTestCase2 { + + TemporaryFolder tempFolder = new TemporaryFolder(); + private LockScreenDeviceIconManager lockScreenDeviceIconManager; + private static final String ICON_URL = "https://i.imgur.com/TgkvOIZ.png"; + private static final String LAST_UPDATED_TIME = "lastUpdatedTime"; + private static final String STORED_PATH = "storedPath"; + + public void setup() throws Exception { + super.setUp(); + } + + public void tearDown() throws Exception { + super.tearDown(); + } + + public void testRetrieveIconShouldCallOnErrorTwiceWhenGivenURLThatCannotDownloadAndIconIsNotCached() { + final SharedPreferences sharedPrefs = Mockito.mock(SharedPreferences.class); + final Context context = Mockito.mock(Context.class); + final LockScreenDeviceIconManager.OnIconRetrievedListener listener = Mockito.mock(LockScreenDeviceIconManager.OnIconRetrievedListener.class); + + Mockito.when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs); + Mockito.when(sharedPrefs.getString(anyString(), (String) isNull())).thenReturn(null); + + lockScreenDeviceIconManager = new LockScreenDeviceIconManager(context); + lockScreenDeviceIconManager.retrieveIcon("", listener); + verify(listener, times(2)).onError(anyString()); + } + + public void testRetrieveIconShouldCallOnImageOnImageRetrievedWithIconWhenIconUpdateTimeIsNullFromSharedPref() { + final SharedPreferences sharedPrefs = Mockito.mock(SharedPreferences.class); + final Context context = Mockito.mock(Context.class); + final LockScreenDeviceIconManager.OnIconRetrievedListener listener = Mockito.mock(LockScreenDeviceIconManager.OnIconRetrievedListener.class); + + Mockito.when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs); + Mockito.when(sharedPrefs.getString(anyString(), (String) isNull())).thenReturn(null); + + lockScreenDeviceIconManager = new LockScreenDeviceIconManager(context); + lockScreenDeviceIconManager.retrieveIcon(ICON_URL, listener); + verify(listener, times(1)).onImageRetrieved((Bitmap) any()); + } + + + public void testRetrieveIconShouldCallOnImageOnImageRetrievedWithIconWhenCachedIconExpired() { + final SharedPreferences sharedPrefs = Mockito.mock(SharedPreferences.class); + final Context context = Mockito.mock(Context.class); + final LockScreenDeviceIconManager.OnIconRetrievedListener listener = Mockito.mock(LockScreenDeviceIconManager.OnIconRetrievedListener.class); + + Mockito.when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs); + Mockito.when(sharedPrefs.getString(anyString(), (String) isNull())).thenReturn(daysToMillisecondsAsString(31)); + + lockScreenDeviceIconManager = new LockScreenDeviceIconManager(context); + lockScreenDeviceIconManager.retrieveIcon(ICON_URL, listener); + verify(listener, times(1)).onImageRetrieved((Bitmap) any()); + } + + public void testRetrieveIconShouldCallOnImageRetrievedWithIconWhenCachedIconIsUpToDate() { + final SharedPreferences sharedPrefs = Mockito.mock(SharedPreferences.class); + final Context context = Mockito.mock(Context.class); + final SharedPreferences.Editor sharedPrefsEditor = Mockito.mock(SharedPreferences.Editor.class); + final LockScreenDeviceIconManager.OnIconRetrievedListener listener = Mockito.mock(LockScreenDeviceIconManager.OnIconRetrievedListener.class); + + Mockito.when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs); + Mockito.when(sharedPrefs.getString(anyString(), (String) isNull())).thenReturn(daysToMillisecondsAsString(15)); + Mockito.when(sharedPrefs.edit()).thenReturn(sharedPrefsEditor); + Mockito.when(sharedPrefsEditor.clear()).thenReturn(sharedPrefsEditor); + + lockScreenDeviceIconManager = new LockScreenDeviceIconManager(context); + lockScreenDeviceIconManager.retrieveIcon(ICON_URL, listener); + verify(listener, times(1)).onImageRetrieved((Bitmap) any()); + } + + private String daysToMillisecondsAsString(int days) { + long milliSeconds = (long) days * 24 * 60 * 60 * 1000; + long previousDay = System.currentTimeMillis() - milliSeconds; + return String.valueOf(previousDay); + } +} diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/managers/lockscreen/LockScreenDeviceIconManager.java b/android/sdl_android/src/main/java/com/smartdevicelink/managers/lockscreen/LockScreenDeviceIconManager.java new file mode 100644 index 000000000..b2b8e6b14 --- /dev/null +++ b/android/sdl_android/src/main/java/com/smartdevicelink/managers/lockscreen/LockScreenDeviceIconManager.java @@ -0,0 +1,214 @@ +package com.smartdevicelink.managers.lockscreen; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import com.smartdevicelink.util.AndroidTools; +import com.smartdevicelink.util.DebugTool; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * <strong>LockScreenDeviceIconManager</strong> <br> + * + * The LockScreenDeviceIconManager handles the logic of caching and retrieving cached lock screen icons <br> + * + */ +class LockScreenDeviceIconManager { + + private Context context; + private static final String SDL_DEVICE_STATUS_SHARED_PREFS = "sdl.lockScreenIcon"; + private static final String STORED_ICON_DIRECTORY_PATH = "sdl/lock_screen_icon/"; + + interface OnIconRetrievedListener { + void onImageRetrieved(Bitmap icon); + void onError(String info); + } + + LockScreenDeviceIconManager(Context context) { + this.context = context; + File lockScreenDirectory = new File(context.getCacheDir(), STORED_ICON_DIRECTORY_PATH); + lockScreenDirectory.mkdirs(); + } + + /** + * Will try to return a lock screen icon either from cache or downloaded + * if it fails iconRetrievedListener.OnError will be called with corresponding error message + * @param iconURL url that the lock screen icon is downloaded from + * @param iconRetrievedListener an interface that will implement onIconReceived and OnError methods + */ + void retrieveIcon(String iconURL, OnIconRetrievedListener iconRetrievedListener) { + Bitmap icon = null; + try { + if (isIconCachedAndValid(iconURL)) { + DebugTool.logInfo("Icon Is Up To Date"); + icon = getFileFromCache(iconURL); + if (icon == null) { + DebugTool.logInfo("Icon from cache was null, attempting to re-download"); + icon = AndroidTools.downloadImage(iconURL); + if (icon != null) { + saveFileToCache(icon, iconURL); + } else { + iconRetrievedListener.onError("Icon downloaded was null"); + return; + } + } + iconRetrievedListener.onImageRetrieved(icon); + } else { + // The icon is unknown or expired. Download the image, save it to the cache, and update the archive file + DebugTool.logInfo("Lock Screen Icon Update Needed"); + icon = AndroidTools.downloadImage(iconURL); + if (icon != null) { + saveFileToCache(icon, iconURL); + iconRetrievedListener.onImageRetrieved(icon); + } else { + iconRetrievedListener.onError("Icon downloaded was null"); + } + } + } catch (IOException e) { + iconRetrievedListener.onError("device Icon Error Downloading, Will attempt to grab cached Icon even if expired: \n" + e.toString()); + icon = getFileFromCache(iconURL); + if (icon != null) { + iconRetrievedListener.onImageRetrieved(icon); + } else { + iconRetrievedListener.onError("Unable to retrieve icon from cache"); + } + } + } + + /** + * Will decide if a cached icon is available and up to date + * @param iconUrl url will be hashed and used to look up last updated timestamp in shared preferences + * @return True when icon details are in shared preferences and less than 30 days old, False if icon details are too old or not found + */ + private boolean isIconCachedAndValid(String iconUrl) { + String iconHash = getMD5HashFromIconUrl(iconUrl); + SharedPreferences sharedPref = this.context.getSharedPreferences(SDL_DEVICE_STATUS_SHARED_PREFS, Context.MODE_PRIVATE); + String iconLastUpdatedTime = sharedPref.getString(iconHash, null); + if(iconLastUpdatedTime == null) { + DebugTool.logInfo("No Icon Details Found In Shared Preferences"); + return false; + } else { + DebugTool.logInfo("Icon Details Found"); + long lastUpdatedTime = 0; + try { + lastUpdatedTime = Long.parseLong(iconLastUpdatedTime); + } catch (NumberFormatException e) { + DebugTool.logInfo("Invalid time stamp stored to shared preferences, clearing cache and share preferences"); + clearIconDirectory(); + sharedPref.edit().clear().commit(); + } + long currentTime = System.currentTimeMillis(); + + long timeDifference = currentTime - lastUpdatedTime; + long daysBetweenLastUpdate = timeDifference / (1000 * 60 * 60 * 24); + return daysBetweenLastUpdate < 30; + } + } + + /** + * Will try to save icon to cache + * @param icon the icon bitmap that should be saved to cache + * @param iconUrl the url where the icon was retrieved will be hashed and used for file and file details lookup + */ + private void saveFileToCache(Bitmap icon, String iconUrl) { + String iconHash = getMD5HashFromIconUrl(iconUrl); + File f = new File(this.context.getCacheDir() + "/" + STORED_ICON_DIRECTORY_PATH, iconHash); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + icon.compress(Bitmap.CompressFormat.PNG, 0 /*ignored for PNG*/, bos); + byte[] bitmapData = bos.toByteArray(); + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(f); + fos.write(bitmapData); + fos.flush(); + fos.close(); + writeDeviceIconParametersToSharedPreferences(iconHash); + } catch (Exception e) { + DebugTool.logError("Failed to save icon to cache"); + e.printStackTrace(); + } + } + + /** + * Will try to retrieve icon bitmap from cached directory + * @param iconUrl the url where the icon was retrieved will be hashed and used to look up file location + * @return bitmap of device icon or null if it fails to find the icon or read from shared preferences + */ + private Bitmap getFileFromCache(String iconUrl) { + String iconHash = getMD5HashFromIconUrl(iconUrl); + SharedPreferences sharedPref = this.context.getSharedPreferences(SDL_DEVICE_STATUS_SHARED_PREFS, Context.MODE_PRIVATE); + String iconLastUpdatedTime = sharedPref.getString(iconHash, null); + + if (iconLastUpdatedTime != null) { + Bitmap cachedIcon = BitmapFactory.decodeFile(this.context.getCacheDir() + "/" + STORED_ICON_DIRECTORY_PATH + "/" + iconHash); + if(cachedIcon == null) { + DebugTool.logError("Failed to get Bitmap from decoding file cache"); + clearIconDirectory(); + sharedPref.edit().clear().commit(); + return null; + } else { + return cachedIcon; + } + } else { + DebugTool.logError("Failed to get shared preferences"); + return null; + } + } + + /** + * Will write information about the icon to shared preferences + * icon information will have a look up key of the hashed icon url and the current timestamp to indicated when the icon was last updated. + * @param iconHash the url where the icon was retrieved will be hashed and used lookup key + */ + private void writeDeviceIconParametersToSharedPreferences(String iconHash) { + SharedPreferences sharedPref = this.context.getSharedPreferences(SDL_DEVICE_STATUS_SHARED_PREFS, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putString(iconHash, String.valueOf(System.currentTimeMillis())); + editor.commit(); + } + + /** + * Create an MD5 hash of the icon url for file storage and lookup/shared preferences look up + * @param iconUrl the url where the icon was retrieved + * @return MD5 hash of the icon URL + */ + private String getMD5HashFromIconUrl(String iconUrl) { + String iconHash = null; + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] messageDigest = md.digest(iconUrl.getBytes()); + BigInteger no = new BigInteger(1, messageDigest); + String hashtext = no.toString(16); + while (hashtext.length() < 32) { + hashtext = "0" + hashtext; + } + iconHash = hashtext; + } catch (NoSuchAlgorithmException e) { + DebugTool.logError("Unable to hash icon url"); + e.printStackTrace(); + } + return iconHash; + } + + /** + * Clears all files in the directory where lock screen icons are cached + */ + private void clearIconDirectory() { + File iconDir = new File(context.getCacheDir() + "/" + STORED_ICON_DIRECTORY_PATH); + if (iconDir.listFiles() != null) { + for (File child : iconDir.listFiles()) { + child.delete(); + } + } + } +} diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/managers/lockscreen/LockScreenManager.java b/android/sdl_android/src/main/java/com/smartdevicelink/managers/lockscreen/LockScreenManager.java index 30ed1b575..2e81894ed 100644 --- a/android/sdl_android/src/main/java/com/smartdevicelink/managers/lockscreen/LockScreenManager.java +++ b/android/sdl_android/src/main/java/com/smartdevicelink/managers/lockscreen/LockScreenManager.java @@ -54,9 +54,8 @@ import com.smartdevicelink.proxy.rpc.enums.LockScreenStatus; import com.smartdevicelink.proxy.rpc.enums.PredefinedWindows; import com.smartdevicelink.proxy.rpc.enums.RequestType; import com.smartdevicelink.proxy.rpc.listeners.OnRPCNotificationListener; -import com.smartdevicelink.util.AndroidTools; +import com.smartdevicelink.util.DebugTool; -import java.io.IOException; import java.lang.ref.WeakReference; /** @@ -82,11 +81,14 @@ public class LockScreenManager extends BaseSubManager { private boolean mLockScreenHasBeenDismissed, lockscreenDismissReceiverRegistered, receivedFirstDDNotification; private String mLockscreenWarningMsg; private BroadcastReceiver mLockscreenDismissedReceiver; + private LockScreenDeviceIconManager mLockScreenDeviceIconManager; public LockScreenManager(LockScreenConfig lockScreenConfig, Context context, ISdl internalInterface){ super(internalInterface); this.context = new WeakReference<>(context); + this.mLockScreenDeviceIconManager = new LockScreenDeviceIconManager(context); + // set initial class variables hmiLevel = HMILevel.HMI_NONE; @@ -231,7 +233,7 @@ public class LockScreenManager extends BaseSubManager { if (msg.getRequestType() == RequestType.LOCK_SCREEN_ICON_URL && msg.getUrl() != null) { // send intent to activity to download icon from core - deviceIconUrl = msg.getUrl(); + deviceIconUrl = msg.getUrl().replace("http://", "https://"); downloadDeviceIcon(deviceIconUrl); } } @@ -375,17 +377,25 @@ public class LockScreenManager extends BaseSubManager { new Thread(new Runnable(){ @Override public void run(){ - try{ - deviceLogo = AndroidTools.downloadImage(url); - Intent intent = new Intent(SDLLockScreenActivity.LOCKSCREEN_DEVICE_LOGO_DOWNLOADED); - intent.putExtra(SDLLockScreenActivity.LOCKSCREEN_DEVICE_LOGO_EXTRA, deviceLogoEnabled); - intent.putExtra(SDLLockScreenActivity.LOCKSCREEN_DEVICE_LOGO_BITMAP, deviceLogo); - if (context.get() != null) { - context.get().sendBroadcast(intent); + mLockScreenDeviceIconManager.retrieveIcon(url, new LockScreenDeviceIconManager.OnIconRetrievedListener() { + @Override + public void onImageRetrieved(Bitmap icon) { + deviceLogo = icon; + if(deviceLogo != null) { + Intent intent = new Intent(SDLLockScreenActivity.LOCKSCREEN_DEVICE_LOGO_DOWNLOADED); + intent.putExtra(SDLLockScreenActivity.LOCKSCREEN_DEVICE_LOGO_EXTRA, deviceLogoEnabled); + intent.putExtra(SDLLockScreenActivity.LOCKSCREEN_DEVICE_LOGO_BITMAP, deviceLogo); + if (context.get() != null) { + context.get().sendBroadcast(intent); + } + } } - }catch(IOException e){ - Log.e(TAG, "device Icon Error Downloading"); - } + + @Override + public void onError(String info) { + DebugTool.logError(info); + } + }); } }).start(); } |