Skip to content

Commit eba22e7

Browse files
Merge pull request #1319 from smartdevicelink/bugfix/issue_1316
Cache Lock Screen Icons Retrieved from URL
2 parents 57989dd + 2714061 commit eba22e7

5 files changed

Lines changed: 349 additions & 14 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,6 @@ build/
7373
/.idea/libraries
7474
/captures
7575
.externalNativeBuild
76-
76+
gradle/
77+
gradlew
78+
gradlew.bat

android/sdl_android/src/androidTest/java/com/smartdevicelink/managers/SdlManagerTests.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.smartdevicelink.transport.BaseTransportConfig;
2626
import com.smartdevicelink.transport.TCPTransportConfig;
2727

28+
import org.mockito.Mockito;
2829
import org.mockito.invocation.InvocationOnMock;
2930
import org.mockito.stubbing.Answer;
3031

@@ -60,6 +61,8 @@ public class SdlManagerTests extends AndroidTestCase2 {
6061
public void setUp() throws Exception{
6162
super.setUp();
6263

64+
mTestContext = Mockito.mock(Context.class);
65+
6366
// set transport
6467
transport = new TCPTransportConfig(TCP_PORT, DEV_MACHINE_IP_ADDRESS, true);
6568

@@ -125,6 +128,7 @@ public LifecycleConfigurationUpdate managerShouldUpdateLifecycle(Language langua
125128
builder.setLockScreenConfig(lockScreenConfig);
126129
builder.setMinimumProtocolVersion(Test.GENERAL_VERSION);
127130
builder.setMinimumRPCVersion(Test.GENERAL_VERSION);
131+
builder.setContext(mTestContext);
128132
manager = builder.build();
129133

130134
// mock SdlProxyBase and set it manually
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package com.smartdevicelink.managers.lockscreen;
2+
3+
import android.content.Context;
4+
import android.content.SharedPreferences;
5+
import android.graphics.Bitmap;
6+
7+
import com.smartdevicelink.AndroidTestCase2;
8+
import com.smartdevicelink.util.AndroidTools;
9+
10+
import org.json.JSONException;
11+
import org.json.JSONObject;
12+
import org.junit.rules.TemporaryFolder;
13+
import org.mockito.Mockito;
14+
15+
import java.io.File;
16+
import java.io.IOException;
17+
import java.math.BigInteger;
18+
import java.security.MessageDigest;
19+
import java.security.NoSuchAlgorithmException;
20+
21+
import static org.mockito.ArgumentMatchers.any;
22+
import static org.mockito.ArgumentMatchers.anyInt;
23+
import static org.mockito.ArgumentMatchers.anyString;
24+
import static org.mockito.ArgumentMatchers.isNull;
25+
import static org.mockito.Mockito.times;
26+
import static org.mockito.Mockito.verify;
27+
28+
public class LockScreenDeviceIconManagerTests extends AndroidTestCase2 {
29+
30+
TemporaryFolder tempFolder = new TemporaryFolder();
31+
private LockScreenDeviceIconManager lockScreenDeviceIconManager;
32+
private static final String ICON_URL = "https://i.imgur.com/TgkvOIZ.png";
33+
private static final String LAST_UPDATED_TIME = "lastUpdatedTime";
34+
private static final String STORED_PATH = "storedPath";
35+
36+
public void setup() throws Exception {
37+
super.setUp();
38+
}
39+
40+
public void tearDown() throws Exception {
41+
super.tearDown();
42+
}
43+
44+
public void testRetrieveIconShouldCallOnErrorTwiceWhenGivenURLThatCannotDownloadAndIconIsNotCached() {
45+
final SharedPreferences sharedPrefs = Mockito.mock(SharedPreferences.class);
46+
final Context context = Mockito.mock(Context.class);
47+
final LockScreenDeviceIconManager.OnIconRetrievedListener listener = Mockito.mock(LockScreenDeviceIconManager.OnIconRetrievedListener.class);
48+
49+
Mockito.when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs);
50+
Mockito.when(sharedPrefs.getString(anyString(), (String) isNull())).thenReturn(null);
51+
52+
lockScreenDeviceIconManager = new LockScreenDeviceIconManager(context);
53+
lockScreenDeviceIconManager.retrieveIcon("", listener);
54+
verify(listener, times(2)).onError(anyString());
55+
}
56+
57+
public void testRetrieveIconShouldCallOnImageOnImageRetrievedWithIconWhenIconUpdateTimeIsNullFromSharedPref() {
58+
final SharedPreferences sharedPrefs = Mockito.mock(SharedPreferences.class);
59+
final Context context = Mockito.mock(Context.class);
60+
final LockScreenDeviceIconManager.OnIconRetrievedListener listener = Mockito.mock(LockScreenDeviceIconManager.OnIconRetrievedListener.class);
61+
62+
Mockito.when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs);
63+
Mockito.when(sharedPrefs.getString(anyString(), (String) isNull())).thenReturn(null);
64+
65+
lockScreenDeviceIconManager = new LockScreenDeviceIconManager(context);
66+
lockScreenDeviceIconManager.retrieveIcon(ICON_URL, listener);
67+
verify(listener, times(1)).onImageRetrieved((Bitmap) any());
68+
}
69+
70+
71+
public void testRetrieveIconShouldCallOnImageOnImageRetrievedWithIconWhenCachedIconExpired() {
72+
final SharedPreferences sharedPrefs = Mockito.mock(SharedPreferences.class);
73+
final Context context = Mockito.mock(Context.class);
74+
final LockScreenDeviceIconManager.OnIconRetrievedListener listener = Mockito.mock(LockScreenDeviceIconManager.OnIconRetrievedListener.class);
75+
76+
Mockito.when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs);
77+
Mockito.when(sharedPrefs.getString(anyString(), (String) isNull())).thenReturn(daysToMillisecondsAsString(31));
78+
79+
lockScreenDeviceIconManager = new LockScreenDeviceIconManager(context);
80+
lockScreenDeviceIconManager.retrieveIcon(ICON_URL, listener);
81+
verify(listener, times(1)).onImageRetrieved((Bitmap) any());
82+
}
83+
84+
public void testRetrieveIconShouldCallOnImageRetrievedWithIconWhenCachedIconIsUpToDate() {
85+
final SharedPreferences sharedPrefs = Mockito.mock(SharedPreferences.class);
86+
final Context context = Mockito.mock(Context.class);
87+
final SharedPreferences.Editor sharedPrefsEditor = Mockito.mock(SharedPreferences.Editor.class);
88+
final LockScreenDeviceIconManager.OnIconRetrievedListener listener = Mockito.mock(LockScreenDeviceIconManager.OnIconRetrievedListener.class);
89+
90+
Mockito.when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs);
91+
Mockito.when(sharedPrefs.getString(anyString(), (String) isNull())).thenReturn(daysToMillisecondsAsString(15));
92+
Mockito.when(sharedPrefs.edit()).thenReturn(sharedPrefsEditor);
93+
Mockito.when(sharedPrefsEditor.clear()).thenReturn(sharedPrefsEditor);
94+
95+
lockScreenDeviceIconManager = new LockScreenDeviceIconManager(context);
96+
lockScreenDeviceIconManager.retrieveIcon(ICON_URL, listener);
97+
verify(listener, times(1)).onImageRetrieved((Bitmap) any());
98+
}
99+
100+
private String daysToMillisecondsAsString(int days) {
101+
long milliSeconds = (long) days * 24 * 60 * 60 * 1000;
102+
long previousDay = System.currentTimeMillis() - milliSeconds;
103+
return String.valueOf(previousDay);
104+
}
105+
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package com.smartdevicelink.managers.lockscreen;
2+
3+
import android.content.Context;
4+
import android.content.SharedPreferences;
5+
import android.graphics.Bitmap;
6+
import android.graphics.BitmapFactory;
7+
8+
import com.smartdevicelink.util.AndroidTools;
9+
import com.smartdevicelink.util.DebugTool;
10+
11+
import java.io.ByteArrayOutputStream;
12+
import java.io.File;
13+
import java.io.FileOutputStream;
14+
import java.io.IOException;
15+
import java.math.BigInteger;
16+
import java.security.MessageDigest;
17+
import java.security.NoSuchAlgorithmException;
18+
19+
/**
20+
* <strong>LockScreenDeviceIconManager</strong> <br>
21+
*
22+
* The LockScreenDeviceIconManager handles the logic of caching and retrieving cached lock screen icons <br>
23+
*
24+
*/
25+
class LockScreenDeviceIconManager {
26+
27+
private Context context;
28+
private static final String SDL_DEVICE_STATUS_SHARED_PREFS = "sdl.lockScreenIcon";
29+
private static final String STORED_ICON_DIRECTORY_PATH = "sdl/lock_screen_icon/";
30+
31+
interface OnIconRetrievedListener {
32+
void onImageRetrieved(Bitmap icon);
33+
void onError(String info);
34+
}
35+
36+
LockScreenDeviceIconManager(Context context) {
37+
this.context = context;
38+
File lockScreenDirectory = new File(context.getCacheDir(), STORED_ICON_DIRECTORY_PATH);
39+
lockScreenDirectory.mkdirs();
40+
}
41+
42+
/**
43+
* Will try to return a lock screen icon either from cache or downloaded
44+
* if it fails iconRetrievedListener.OnError will be called with corresponding error message
45+
* @param iconURL url that the lock screen icon is downloaded from
46+
* @param iconRetrievedListener an interface that will implement onIconReceived and OnError methods
47+
*/
48+
void retrieveIcon(String iconURL, OnIconRetrievedListener iconRetrievedListener) {
49+
Bitmap icon = null;
50+
try {
51+
if (isIconCachedAndValid(iconURL)) {
52+
DebugTool.logInfo("Icon Is Up To Date");
53+
icon = getFileFromCache(iconURL);
54+
if (icon == null) {
55+
DebugTool.logInfo("Icon from cache was null, attempting to re-download");
56+
icon = AndroidTools.downloadImage(iconURL);
57+
if (icon != null) {
58+
saveFileToCache(icon, iconURL);
59+
} else {
60+
iconRetrievedListener.onError("Icon downloaded was null");
61+
return;
62+
}
63+
}
64+
iconRetrievedListener.onImageRetrieved(icon);
65+
} else {
66+
// The icon is unknown or expired. Download the image, save it to the cache, and update the archive file
67+
DebugTool.logInfo("Lock Screen Icon Update Needed");
68+
icon = AndroidTools.downloadImage(iconURL);
69+
if (icon != null) {
70+
saveFileToCache(icon, iconURL);
71+
iconRetrievedListener.onImageRetrieved(icon);
72+
} else {
73+
iconRetrievedListener.onError("Icon downloaded was null");
74+
}
75+
}
76+
} catch (IOException e) {
77+
iconRetrievedListener.onError("device Icon Error Downloading, Will attempt to grab cached Icon even if expired: \n" + e.toString());
78+
icon = getFileFromCache(iconURL);
79+
if (icon != null) {
80+
iconRetrievedListener.onImageRetrieved(icon);
81+
} else {
82+
iconRetrievedListener.onError("Unable to retrieve icon from cache");
83+
}
84+
}
85+
}
86+
87+
/**
88+
* Will decide if a cached icon is available and up to date
89+
* @param iconUrl url will be hashed and used to look up last updated timestamp in shared preferences
90+
* @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
91+
*/
92+
private boolean isIconCachedAndValid(String iconUrl) {
93+
String iconHash = getMD5HashFromIconUrl(iconUrl);
94+
SharedPreferences sharedPref = this.context.getSharedPreferences(SDL_DEVICE_STATUS_SHARED_PREFS, Context.MODE_PRIVATE);
95+
String iconLastUpdatedTime = sharedPref.getString(iconHash, null);
96+
if(iconLastUpdatedTime == null) {
97+
DebugTool.logInfo("No Icon Details Found In Shared Preferences");
98+
return false;
99+
} else {
100+
DebugTool.logInfo("Icon Details Found");
101+
long lastUpdatedTime = 0;
102+
try {
103+
lastUpdatedTime = Long.parseLong(iconLastUpdatedTime);
104+
} catch (NumberFormatException e) {
105+
DebugTool.logInfo("Invalid time stamp stored to shared preferences, clearing cache and share preferences");
106+
clearIconDirectory();
107+
sharedPref.edit().clear().commit();
108+
}
109+
long currentTime = System.currentTimeMillis();
110+
111+
long timeDifference = currentTime - lastUpdatedTime;
112+
long daysBetweenLastUpdate = timeDifference / (1000 * 60 * 60 * 24);
113+
return daysBetweenLastUpdate < 30;
114+
}
115+
}
116+
117+
/**
118+
* Will try to save icon to cache
119+
* @param icon the icon bitmap that should be saved to cache
120+
* @param iconUrl the url where the icon was retrieved will be hashed and used for file and file details lookup
121+
*/
122+
private void saveFileToCache(Bitmap icon, String iconUrl) {
123+
String iconHash = getMD5HashFromIconUrl(iconUrl);
124+
File f = new File(this.context.getCacheDir() + "/" + STORED_ICON_DIRECTORY_PATH, iconHash);
125+
ByteArrayOutputStream bos = new ByteArrayOutputStream();
126+
icon.compress(Bitmap.CompressFormat.PNG, 0 /*ignored for PNG*/, bos);
127+
byte[] bitmapData = bos.toByteArray();
128+
129+
FileOutputStream fos = null;
130+
try {
131+
fos = new FileOutputStream(f);
132+
fos.write(bitmapData);
133+
fos.flush();
134+
fos.close();
135+
writeDeviceIconParametersToSharedPreferences(iconHash);
136+
} catch (Exception e) {
137+
DebugTool.logError("Failed to save icon to cache");
138+
e.printStackTrace();
139+
}
140+
}
141+
142+
/**
143+
* Will try to retrieve icon bitmap from cached directory
144+
* @param iconUrl the url where the icon was retrieved will be hashed and used to look up file location
145+
* @return bitmap of device icon or null if it fails to find the icon or read from shared preferences
146+
*/
147+
private Bitmap getFileFromCache(String iconUrl) {
148+
String iconHash = getMD5HashFromIconUrl(iconUrl);
149+
SharedPreferences sharedPref = this.context.getSharedPreferences(SDL_DEVICE_STATUS_SHARED_PREFS, Context.MODE_PRIVATE);
150+
String iconLastUpdatedTime = sharedPref.getString(iconHash, null);
151+
152+
if (iconLastUpdatedTime != null) {
153+
Bitmap cachedIcon = BitmapFactory.decodeFile(this.context.getCacheDir() + "/" + STORED_ICON_DIRECTORY_PATH + "/" + iconHash);
154+
if(cachedIcon == null) {
155+
DebugTool.logError("Failed to get Bitmap from decoding file cache");
156+
clearIconDirectory();
157+
sharedPref.edit().clear().commit();
158+
return null;
159+
} else {
160+
return cachedIcon;
161+
}
162+
} else {
163+
DebugTool.logError("Failed to get shared preferences");
164+
return null;
165+
}
166+
}
167+
168+
/**
169+
* Will write information about the icon to shared preferences
170+
* 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.
171+
* @param iconHash the url where the icon was retrieved will be hashed and used lookup key
172+
*/
173+
private void writeDeviceIconParametersToSharedPreferences(String iconHash) {
174+
SharedPreferences sharedPref = this.context.getSharedPreferences(SDL_DEVICE_STATUS_SHARED_PREFS, Context.MODE_PRIVATE);
175+
SharedPreferences.Editor editor = sharedPref.edit();
176+
editor.putString(iconHash, String.valueOf(System.currentTimeMillis()));
177+
editor.commit();
178+
}
179+
180+
/**
181+
* Create an MD5 hash of the icon url for file storage and lookup/shared preferences look up
182+
* @param iconUrl the url where the icon was retrieved
183+
* @return MD5 hash of the icon URL
184+
*/
185+
private String getMD5HashFromIconUrl(String iconUrl) {
186+
String iconHash = null;
187+
try {
188+
MessageDigest md = MessageDigest.getInstance("MD5");
189+
byte[] messageDigest = md.digest(iconUrl.getBytes());
190+
BigInteger no = new BigInteger(1, messageDigest);
191+
String hashtext = no.toString(16);
192+
while (hashtext.length() < 32) {
193+
hashtext = "0" + hashtext;
194+
}
195+
iconHash = hashtext;
196+
} catch (NoSuchAlgorithmException e) {
197+
DebugTool.logError("Unable to hash icon url");
198+
e.printStackTrace();
199+
}
200+
return iconHash;
201+
}
202+
203+
/**
204+
* Clears all files in the directory where lock screen icons are cached
205+
*/
206+
private void clearIconDirectory() {
207+
File iconDir = new File(context.getCacheDir() + "/" + STORED_ICON_DIRECTORY_PATH);
208+
if (iconDir.listFiles() != null) {
209+
for (File child : iconDir.listFiles()) {
210+
child.delete();
211+
}
212+
}
213+
}
214+
}

0 commit comments

Comments
 (0)