Skip to content

Commit cc384bf

Browse files
Merge pull request #1632 from microsoft/fix/protected-context
Fix crash when direct boot
2 parents 17507e4 + 72d8cdb commit cc384bf

File tree

7 files changed

+342
-109
lines changed

7 files changed

+342
-109
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Version 4.4.4 (Under active development)
44

5+
### App Center
6+
7+
* **[Fix]** Fix crash when storage is encrypted during direct boot. Please note that settings and pending logs database are not shared between regular and device-protected storage.
8+
59
### App Center Distribute
610

711
* **[Improvement]** Remove optional `SYSTEM_ALERT_WINDOW` permission that was required to automatically restart the app after installing the update.

sdk/appcenter/src/main/java/com/microsoft/appcenter/AppCenter.java

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252

5353
import static android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE;
5454
import static android.util.Log.VERBOSE;
55+
import static com.microsoft.appcenter.ApplicationContextUtils.getApplicationContext;
56+
import static com.microsoft.appcenter.ApplicationContextUtils.isDeviceProtectedStorage;
5557
import static com.microsoft.appcenter.Constants.DEFAULT_TRIGGER_COUNT;
5658
import static com.microsoft.appcenter.Constants.DEFAULT_TRIGGER_INTERVAL;
5759
import static com.microsoft.appcenter.Constants.DEFAULT_TRIGGER_MAX_PARALLEL_REQUESTS;
@@ -135,10 +137,18 @@ public class AppCenter {
135137
private String mLogUrl;
136138

137139
/**
138-
* Application context.
140+
* Android application object that was passed during configuration.
141+
* It shouldn't be used directly, but only for getting right context.
142+
* It's null until SDK is configured.
139143
*/
140144
private Application mApplication;
141145

146+
/**
147+
* Application context. Might be special context with device-protected storage.
148+
* See {@link ApplicationContextUtils#getApplicationContext(Application)}.
149+
*/
150+
private Context mContext;
151+
142152
/**
143153
* Application lifecycle listener.
144154
*/
@@ -710,8 +720,20 @@ public void run() {
710720
return true;
711721
}
712722

713-
/* Store state. */
723+
/* Store application to use it later for registering services as lifecycle callbacks. */
714724
mApplication = application;
725+
mContext = getApplicationContext(application);
726+
if (isDeviceProtectedStorage(mContext)) {
727+
728+
/*
729+
* In this mode storing sensitive is strongly discouraged, but App Center considers regular storage as
730+
* not secure as well, so all tokens are encrypted separately anyway. Just warn about it.
731+
*/
732+
AppCenterLog.warn(LOG_TAG,
733+
"A user is locked, credential-protected private app data storage is not available.\n" +
734+
"App Center will use device-protected data storage that available without user authentication.\n" +
735+
"Please note that it's a separate storage, all settings and pending logs won't be shared with regular storage.");
736+
}
715737

716738
/* Start looper. */
717739
mHandlerThread = new HandlerThread("AppCenter.Looper");
@@ -825,11 +847,11 @@ public void run() {
825847
private void finishConfiguration(boolean configureFromApp) {
826848

827849
/* Load some global constants. */
828-
Constants.loadFromContext(mApplication);
850+
Constants.loadFromContext(mContext);
829851

830852
/* If parameters are valid, init context related resources. */
831-
FileManager.initialize(mApplication);
832-
SharedPreferencesManager.initialize(mApplication);
853+
FileManager.initialize(mContext);
854+
SharedPreferencesManager.initialize(mContext);
833855

834856
/* Set network requests allowed. */
835857
if (mAllowedNetworkRequests != null) {
@@ -845,13 +867,13 @@ private void finishConfiguration(boolean configureFromApp) {
845867
/* Instantiate HTTP client if it doesn't exist as a dependency. */
846868
HttpClient httpClient = DependencyConfiguration.getHttpClient();
847869
if (httpClient == null) {
848-
httpClient = createHttpClient(mApplication);
870+
httpClient = createHttpClient(mContext);
849871
}
850872

851873
/* Init channel. */
852874
mLogSerializer = new DefaultLogSerializer();
853875
mLogSerializer.addLogFactory(StartServiceLog.TYPE, new StartServiceLogFactory());
854-
mChannel = new DefaultChannel(mApplication, mAppSecret, mLogSerializer, httpClient, mHandler);
876+
mChannel = new DefaultChannel(mContext, mAppSecret, mLogSerializer, httpClient, mHandler);
855877

856878
/* Complete set maximum storage size future if starting from app. */
857879
if (configureFromApp) {
@@ -877,7 +899,7 @@ private void finishConfiguration(boolean configureFromApp) {
877899

878900
/* Disable listening network if we start while being disabled. */
879901
if (!enabled) {
880-
NetworkStateHelper.getSharedInstance(mApplication).close();
902+
NetworkStateHelper.getSharedInstance(mContext).close();
881903
}
882904

883905
/* Init uncaught exception handler. */
@@ -902,7 +924,7 @@ private final synchronized void startServices(final boolean startFromApp, Class<
902924
AppCenterLog.error(LOG_TAG, "Cannot start services, services array is null. Failed to start services.");
903925
return;
904926
}
905-
if (mApplication == null) {
927+
if (!isInstanceConfigured()) {
906928
StringBuilder serviceNames = new StringBuilder();
907929
for (Class<? extends AppCenterService> service : services) {
908930
serviceNames.append("\t").append(service.getName()).append("\n");
@@ -1011,10 +1033,10 @@ private void finishStartServices(Iterable<AppCenterService> updatedServices, Ite
10111033
service.setInstanceEnabled(false);
10121034
}
10131035
if (startFromApp) {
1014-
service.onStarted(mApplication, mChannel, mAppSecret, mTransmissionTargetToken, true);
1036+
service.onStarted(mContext, mChannel, mAppSecret, mTransmissionTargetToken, true);
10151037
AppCenterLog.info(LOG_TAG, service.getClass().getSimpleName() + " service started from application.");
10161038
} else {
1017-
service.onStarted(mApplication, mChannel, null, null, false);
1039+
service.onStarted(mContext, mChannel, null, null, false);
10181040
AppCenterLog.info(LOG_TAG, service.getClass().getSimpleName() + " service started from library.");
10191041
}
10201042
}
@@ -1118,10 +1140,10 @@ private void setInstanceEnabled(boolean enabled) {
11181140
/* Update uncaught exception subscription. */
11191141
if (switchToEnabled) {
11201142
mUncaughtExceptionHandler.register();
1121-
NetworkStateHelper.getSharedInstance(mApplication).reopen();
1143+
NetworkStateHelper.getSharedInstance(mContext).reopen();
11221144
} else if (switchToDisabled) {
11231145
mUncaughtExceptionHandler.unregister();
1124-
NetworkStateHelper.getSharedInstance(mApplication).close();
1146+
NetworkStateHelper.getSharedInstance(mContext).close();
11251147
}
11261148

11271149
/* Update state now if true, services are checking this. */
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
package com.microsoft.appcenter;
7+
8+
import android.app.Application;
9+
import android.content.Context;
10+
import android.os.Build;
11+
import android.os.UserManager;
12+
13+
/**
14+
* Helper application context utility class to deal with direct boot where regular storage is not available.
15+
*/
16+
class ApplicationContextUtils {
17+
18+
/**
19+
* Get application context with device-protected storage if needed.
20+
* Note that this method might return a new instance of device-protected storage context object each call.
21+
* See {@link Context#createDeviceProtectedStorageContext()}.
22+
*
23+
* @param application android application.
24+
* @return application context with device-protected storage if needed.
25+
*/
26+
static Context getApplicationContext(Application application) {
27+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
28+
UserManager userManager = (UserManager) application.getSystemService(Context.USER_SERVICE);
29+
30+
/*
31+
* On devices with direct boot, a user is unlocked only after they've entered
32+
* their credentials (such as a lock pattern or PIN).
33+
*/
34+
if (!userManager.isUserUnlocked()) {
35+
return application.createDeviceProtectedStorageContext();
36+
}
37+
}
38+
return application.getApplicationContext();
39+
}
40+
41+
/**
42+
* Indicates if the storage APIs of this Context are backed by device-protected storage.
43+
*
44+
* @param context context to check.
45+
* @return true if the storage APIs of this Context are backed by device-protected storage.
46+
*/
47+
static boolean isDeviceProtectedStorage(Context context) {
48+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
49+
return context.isDeviceProtectedStorage();
50+
}
51+
return false;
52+
}
53+
}

sdk/appcenter/src/test/java/com/microsoft/appcenter/AbstractAppCenterTest.java

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@
55

66
package com.microsoft.appcenter;
77

8+
import static com.microsoft.appcenter.AppCenter.KEY_VALUE_DELIMITER;
9+
import static com.microsoft.appcenter.AppCenter.TRANSMISSION_TARGET_TOKEN_KEY;
10+
import static org.mockito.ArgumentMatchers.any;
11+
import static org.mockito.ArgumentMatchers.anyBoolean;
12+
import static org.mockito.ArgumentMatchers.anyString;
13+
import static org.mockito.ArgumentMatchers.eq;
14+
import static org.mockito.Mockito.mock;
15+
import static org.mockito.Mockito.spy;
16+
import static org.mockito.Mockito.when;
17+
import static org.powermock.api.mockito.PowerMockito.doAnswer;
18+
import static org.powermock.api.mockito.PowerMockito.mockStatic;
19+
import static org.powermock.api.mockito.PowerMockito.whenNew;
20+
821
import android.app.Application;
922
import android.content.Context;
1023
import android.content.pm.ApplicationInfo;
@@ -43,35 +56,23 @@
4356
import java.util.HashMap;
4457
import java.util.Map;
4558

46-
import static com.microsoft.appcenter.AppCenter.KEY_VALUE_DELIMITER;
47-
import static com.microsoft.appcenter.AppCenter.TRANSMISSION_TARGET_TOKEN_KEY;
48-
import static org.mockito.ArgumentMatchers.any;
49-
import static org.mockito.ArgumentMatchers.anyBoolean;
50-
import static org.mockito.ArgumentMatchers.anyString;
51-
import static org.mockito.ArgumentMatchers.eq;
52-
import static org.mockito.Mockito.mock;
53-
import static org.mockito.Mockito.spy;
54-
import static org.mockito.Mockito.when;
55-
import static org.powermock.api.mockito.PowerMockito.doAnswer;
56-
import static org.powermock.api.mockito.PowerMockito.mockStatic;
57-
import static org.powermock.api.mockito.PowerMockito.whenNew;
58-
5959
@PrepareForTest({
6060
AppCenter.class,
61-
UncaughtExceptionHandler.class,
62-
DefaultChannel.class,
63-
Constants.class,
6461
AppCenterLog.class,
65-
StartServiceLog.class,
62+
ApplicationContextUtils.class,
63+
Constants.class,
64+
DefaultChannel.class,
65+
DeviceInfoHelper.class,
6666
FileManager.class,
67-
SharedPreferencesManager.class,
6867
IdHelper.class,
69-
DeviceInfoHelper.class,
70-
Thread.class,
71-
ShutdownHelper.class,
7268
InstrumentationRegistryHelper.class,
69+
JSONUtils.class,
7370
NetworkStateHelper.class,
74-
JSONUtils.class
71+
SharedPreferencesManager.class,
72+
ShutdownHelper.class,
73+
StartServiceLog.class,
74+
Thread.class,
75+
UncaughtExceptionHandler.class
7576
})
7677
public class AbstractAppCenterTest {
7778

@@ -84,6 +85,9 @@ public class AbstractAppCenterTest {
8485
@Rule
8586
public PowerMockRule mPowerMockRule = new PowerMockRule();
8687

88+
@Mock
89+
Context mContext;
90+
8791
@Mock
8892
DefaultChannel mChannel;
8993

@@ -124,6 +128,12 @@ public void setUp() throws Exception {
124128
mApplicationInfo.flags = ApplicationInfo.FLAG_DEBUGGABLE;
125129
when(mApplication.getApplicationInfo()).thenReturn(mApplicationInfo);
126130

131+
/* Mock ApplicationContextUtils. */
132+
mockStatic(ApplicationContextUtils.class);
133+
when(ApplicationContextUtils.getApplicationContext(mApplication)).thenReturn(mContext);
134+
when(ApplicationContextUtils.isDeviceProtectedStorage(mContext)).thenReturn(false);
135+
136+
/* Mock static classes. */
127137
mockStatic(Constants.class);
128138
mockStatic(AppCenterLog.class);
129139
mockStatic(FileManager.class);

sdk/appcenter/src/test/java/com/microsoft/appcenter/AppCenterLibraryTest.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,6 @@
55

66
package com.microsoft.appcenter;
77

8-
import android.content.Context;
9-
10-
import com.microsoft.appcenter.channel.Channel;
11-
import com.microsoft.appcenter.channel.OneCollectorChannelListener;
12-
import com.microsoft.appcenter.utils.AppCenterLog;
13-
14-
import org.junit.Test;
15-
16-
import java.util.ArrayList;
17-
import java.util.List;
18-
198
import static com.microsoft.appcenter.AppCenter.CORE_GROUP;
209
import static com.microsoft.appcenter.AppCenter.LOG_TAG;
2110
import static com.microsoft.appcenter.AppCenter.PAIR_DELIMITER;
@@ -35,6 +24,17 @@
3524
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
3625
import static org.powermock.api.mockito.PowerMockito.whenNew;
3726

27+
import android.content.Context;
28+
29+
import com.microsoft.appcenter.channel.Channel;
30+
import com.microsoft.appcenter.channel.OneCollectorChannelListener;
31+
import com.microsoft.appcenter.utils.AppCenterLog;
32+
33+
import org.junit.Test;
34+
35+
import java.util.ArrayList;
36+
import java.util.List;
37+
3838
public class AppCenterLibraryTest extends AbstractAppCenterTest {
3939

4040
@Test
@@ -347,7 +347,7 @@ public void startFromLibraryDoesNotStartFromApp() {
347347
AppCenter.startFromLibrary(mApplication, DummyService.class);
348348

349349
/* Verify second service started without secrets with library flag. */
350-
verify(DummyService.getInstance()).onStarted(mApplication, mChannel, null, null, false);
350+
verify(DummyService.getInstance()).onStarted(mContext, mChannel, null, null, false);
351351

352352
/* Now start from app. */
353353
AppCenter.start(DummyService.class);

0 commit comments

Comments
 (0)