Merge "Migrate from SkCanvas::getGrContext to SkCanvas::recordingContext"
diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING
index 4139b2f..b0307d1 100644
--- a/core/java/android/app/TEST_MAPPING
+++ b/core/java/android/app/TEST_MAPPING
@@ -70,7 +70,7 @@
"name": "CtsAutoFillServiceTestCases",
"options": [
{
- "include-filter": "android.autofillservice.cts.PreSimpleSaveActivityTest"
+ "include-filter": "android.autofillservice.cts.saveui.PreSimpleSaveActivityTest"
},
{
"exclude-annotation": "androidx.test.filters.FlakyTest"
@@ -82,7 +82,7 @@
"name": "CtsAutoFillServiceTestCases",
"options": [
{
- "include-filter": "android.autofillservice.cts.SimpleSaveActivityTest"
+ "include-filter": "android.autofillservice.cts.saveui.SimpleSaveActivityTest"
},
{
"exclude-annotation": "androidx.test.filters.FlakyTest"
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 0827fef..49bc65b 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -1326,7 +1326,8 @@
line2 = res.getString(R.string.zen_mode_until, formattedTime);
} else {
// display as day/time
- summary = line1 = line2 = res.getString(R.string.zen_mode_until, formattedTime);
+ summary = line1 = line2 = res.getString(R.string.zen_mode_until_next_day,
+ formattedTime);
}
final Uri id = toCountdownConditionId(time, false);
return new Condition(id, summary, line1, line2, 0, Condition.STATE_TRUE,
diff --git a/core/java/com/android/internal/infra/AbstractRemoteService.java b/core/java/com/android/internal/infra/AbstractRemoteService.java
index b2852ea..722e5c1 100644
--- a/core/java/com/android/internal/infra/AbstractRemoteService.java
+++ b/core/java/com/android/internal/infra/AbstractRemoteService.java
@@ -225,6 +225,7 @@
if (mService != null) {
mService.asBinder().unlinkToDeath(this, 0);
}
+ mBinding = false;
mService = null;
mServiceDied = true;
cancelScheduledUnbind();
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index a5e5fba..03975da 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -4888,6 +4888,9 @@
<item quantity="other">For %d hr</item>
</plurals>
+ <!-- Zen mode condition - line two: ending time indicating the next day. [CHAR LIMIT=NONE] -->
+ <string name="zen_mode_until_next_day">Until <xliff:g id="formattedTime" example="Tue, 10 PM">%1$s</xliff:g></string>
+
<!-- Zen mode condition - line two: ending time. [CHAR LIMIT=NONE] -->
<string name="zen_mode_until">Until <xliff:g id="formattedTime" example="10:00 PM">%1$s</xliff:g></string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index fdcd39a..8cd4b48 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2393,6 +2393,7 @@
<java-symbol type="plurals" name="zen_mode_duration_hours_short" />
<java-symbol type="plurals" name="zen_mode_duration_minutes_summary_short" />
<java-symbol type="plurals" name="zen_mode_duration_hours_summary_short" />
+ <java-symbol type="string" name="zen_mode_until_next_day" />
<java-symbol type="string" name="zen_mode_until" />
<java-symbol type="string" name="zen_mode_feature_name" />
<java-symbol type="string" name="zen_mode_downtime_feature_name" />
diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING
index 6dc0c6f..cf2f0f0 100644
--- a/media/TEST_MAPPING
+++ b/media/TEST_MAPPING
@@ -4,21 +4,13 @@
"name": "GtsMediaTestCases",
"options" : [
{
- "include-annotation": "android.platform.test.annotations.Presubmit"
+ "include-annotation": "android.platform.test.annotations.Presubmit"
},
{
"include-filter": "com.google.android.media.gts.WidevineGenericOpsTests"
- }
- ]
- },
- {
- "name": "GtsExoPlayerTestCases",
- "options" : [
- {
- "include-annotation": "android.platform.test.annotations.SocPresubmit"
},
{
- "include-filter": "com.google.android.exoplayer.gts.DashTest#testWidevine23FpsH264Fixed"
+ "include-filter": "com.google.android.media.gts.WidevineYouTubePerformanceTests"
}
]
}
diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index 5db6729..5daf8b0 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -171,6 +171,12 @@
void DestroyCallback(const C2Buffer * /* buf */, void *arg) {
android::sp<android::MediaEvent> event = (android::MediaEvent *)arg;
+ if (event->mLinearBlockObj != NULL) {
+ JNIEnv *env = android::AndroidRuntime::getJNIEnv();
+ env->DeleteWeakGlobalRef(event->mLinearBlockObj);
+ event->mLinearBlockObj = NULL;
+ }
+
event->mAvHandleRefCnt--;
event->finalize();
}
@@ -182,6 +188,12 @@
mLnb = env->NewWeakGlobalRef(lnbObj);
}
+LnbCallback::~LnbCallback() {
+ JNIEnv *env = AndroidRuntime::getJNIEnv();
+ env->DeleteWeakGlobalRef(mLnb);
+ mLnb = NULL;
+}
+
Return<void> LnbCallback::onEvent(LnbEventType lnbEventType) {
ALOGD("LnbCallback::onEvent, type=%d", lnbEventType);
JNIEnv *env = AndroidRuntime::getJNIEnv();
@@ -305,6 +317,7 @@
JNIEnv *env = AndroidRuntime::getJNIEnv();
mMediaEventObj = env->NewWeakGlobalRef(obj);
mAvHandle = native_handle_clone(avHandle.getNativeHandle());
+ mLinearBlockObj = NULL;
}
MediaEvent::~MediaEvent() {
@@ -367,7 +380,7 @@
true);
mLinearBlockObj = env->NewWeakGlobalRef(linearBlock);
mAvHandleRefCnt++;
- return mLinearBlockObj;
+ return linearBlock;
} else {
native_handle_close(const_cast<native_handle_t*>(
reinterpret_cast<const native_handle_t*>(mIonHandle)));
diff --git a/media/jni/android_media_tv_Tuner.h b/media/jni/android_media_tv_Tuner.h
index fd29959..c4deeaf 100644
--- a/media/jni/android_media_tv_Tuner.h
+++ b/media/jni/android_media_tv_Tuner.h
@@ -73,6 +73,7 @@
struct LnbCallback : public ILnbCallback {
LnbCallback(jweak tunerObj, LnbId id);
+ ~LnbCallback();
virtual Return<void> onEvent(LnbEventType lnbEventType);
virtual Return<void> onDiseqcMessage(const hidl_vec<uint8_t>& diseqcMessage);
jweak mLnb;
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index e246917..26cead2 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -86,6 +86,10 @@
*/
InstanceId getInstanceId();
+ default boolean isTileReady() {
+ return false;
+ }
+
@ProvidesInterface(version = Callback.VERSION)
public interface Callback {
public static final int VERSION = 1;
diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
index a3339f6..0fd4765 100644
--- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
@@ -16,8 +16,13 @@
package com.android.systemui.appops;
+import static android.media.AudioManager.ACTION_MICROPHONE_MUTE_CHANGED;
+
import android.app.AppOpsManager;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import android.media.AudioManager;
@@ -34,6 +39,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.Dumpable;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
@@ -54,7 +60,7 @@
* NotificationPresenter to be displayed to the user.
*/
@SysUISingleton
-public class AppOpsControllerImpl implements AppOpsController,
+public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsController,
AppOpsManager.OnOpActiveChangedInternalListener,
AppOpsManager.OnOpNotedListener, Dumpable {
@@ -65,6 +71,7 @@
private static final String TAG = "AppOpsControllerImpl";
private static final boolean DEBUG = false;
+ private final BroadcastDispatcher mDispatcher;
private final AppOpsManager mAppOps;
private final AudioManager mAudioManager;
private final LocationManager mLocationManager;
@@ -79,6 +86,7 @@
private final SparseArray<Set<Callback>> mCallbacksByCode = new SparseArray<>();
private final PermissionFlagsCache mFlagsCache;
private boolean mListening;
+ private boolean mMicMuted;
@GuardedBy("mActiveItems")
private final List<AppOpItem> mActiveItems = new ArrayList<>();
@@ -105,8 +113,10 @@
@Background Looper bgLooper,
DumpManager dumpManager,
PermissionFlagsCache cache,
- AudioManager audioManager
+ AudioManager audioManager,
+ BroadcastDispatcher dispatcher
) {
+ mDispatcher = dispatcher;
mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
mFlagsCache = cache;
mBGHandler = new H(bgLooper);
@@ -115,6 +125,7 @@
mCallbacksByCode.put(OPS[i], new ArraySet<>());
}
mAudioManager = audioManager;
+ mMicMuted = audioManager.isMicrophoneMute();
mLocationManager = context.getSystemService(LocationManager.class);
dumpManager.registerDumpable(TAG, this);
}
@@ -133,6 +144,8 @@
mAudioManager.registerAudioRecordingCallback(mAudioRecordingCallback, mBGHandler);
mBGHandler.post(() -> mAudioRecordingCallback.onRecordingConfigChanged(
mAudioManager.getActiveRecordingConfigurations()));
+ mDispatcher.registerReceiverWithHandler(this,
+ new IntentFilter(ACTION_MICROPHONE_MUTE_CHANGED), mBGHandler);
} else {
mAppOps.stopWatchingActive(this);
@@ -140,6 +153,7 @@
mAudioManager.unregisterAudioRecordingCallback(mAudioRecordingCallback);
mBGHandler.removeCallbacksAndMessages(null); // null removes all
+ mDispatcher.unregisterReceiver(this);
synchronized (mActiveItems) {
mActiveItems.clear();
mRecordingsByUid.clear();
@@ -468,6 +482,9 @@
}
private boolean isAnyRecordingPausedLocked(int uid) {
+ if (mMicMuted) {
+ return true;
+ }
List<AudioRecordingConfiguration> configs = mRecordingsByUid.get(uid);
if (configs == null) return false;
int configsNum = configs.size();
@@ -522,6 +539,12 @@
}
};
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mMicMuted = mAudioManager.isMicrophoneMute();
+ updateRecordingPausedStatus();
+ }
+
protected class H extends Handler {
H(Looper looper) {
super(looper);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 06c190f..ba78485 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -16,17 +16,16 @@
package com.android.systemui.biometrics;
-import android.annotation.NonNull;
+import static com.android.internal.util.Preconditions.checkNotNull;
+
import android.annotation.SuppressLint;
-import android.content.ContentResolver;
import android.content.Context;
+import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.IUdfpsOverlayController;
-import android.os.Handler;
-import android.os.Looper;
import android.os.PowerManager;
import android.os.UserHandle;
import android.provider.Settings;
@@ -38,10 +37,16 @@
import android.view.MotionEvent;
import android.view.WindowManager;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.internal.BrightnessSynchronizer;
import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.doze.DozeReceiver;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.util.settings.SystemSettings;
import java.io.FileWriter;
import java.io.IOException;
@@ -60,8 +65,8 @@
private final FingerprintManager mFingerprintManager;
private final WindowManager mWindowManager;
- private final ContentResolver mContentResolver;
- private final Handler mHandler;
+ private final SystemSettings mSystemSettings;
+ private final DelayableExecutor mFgExecutor;
private final WindowManager.LayoutParams mLayoutParams;
private final UdfpsView mView;
// Debugfs path to control the high-brightness mode.
@@ -88,7 +93,7 @@
// interrupt is being tracked and a timeout is used as a last resort to turn off high brightness
// mode.
private boolean mIsAodInterruptActive;
- private final Runnable mAodInterruptTimeoutAction = this::onCancelAodInterrupt;
+ @Nullable private Runnable mCancelAodTimeoutAction;
public class UdfpsOverlayController extends IUdfpsOverlayController.Stub {
@Override
@@ -138,18 +143,27 @@
@Inject
UdfpsController(@NonNull Context context,
- @NonNull StatusBarStateController statusBarStateController) {
- mFingerprintManager = context.getSystemService(FingerprintManager.class);
- mWindowManager = context.getSystemService(WindowManager.class);
- mContentResolver = context.getContentResolver();
- mHandler = new Handler(Looper.getMainLooper());
+ @Main Resources resources,
+ LayoutInflater inflater,
+ @Nullable FingerprintManager fingerprintManager,
+ PowerManager powerManager,
+ WindowManager windowManager,
+ SystemSettings systemSettings,
+ @NonNull StatusBarStateController statusBarStateController,
+ @Main DelayableExecutor fgExecutor) {
+ // The fingerprint manager is queried for UDFPS before this class is constructed, so the
+ // fingerprint manager should never be null.
+ mFingerprintManager = checkNotNull(fingerprintManager);
+ mWindowManager = windowManager;
+ mSystemSettings = systemSettings;
+ mFgExecutor = fgExecutor;
mLayoutParams = createLayoutParams(context);
- mView = (UdfpsView) LayoutInflater.from(context).inflate(R.layout.udfps_view, null, false);
+ mView = (UdfpsView) inflater.inflate(R.layout.udfps_view, null, false);
- mHbmPath = context.getResources().getString(R.string.udfps_hbm_sysfs_path);
- mHbmEnableCommand = context.getResources().getString(R.string.udfps_hbm_enable_command);
- mHbmDisableCommand = context.getResources().getString(R.string.udfps_hbm_disable_command);
+ mHbmPath = resources.getString(R.string.udfps_hbm_sysfs_path);
+ mHbmEnableCommand = resources.getString(R.string.udfps_hbm_enable_command);
+ mHbmDisableCommand = resources.getString(R.string.udfps_hbm_disable_command);
mHbmSupported = !TextUtils.isEmpty(mHbmPath);
mView.setHbmSupported(mHbmSupported);
@@ -157,11 +171,11 @@
// This range only consists of the minimum and maximum values, which only cover
// non-high-brightness mode.
- float[] nitsRange = toFloatArray(context.getResources().obtainTypedArray(
+ float[] nitsRange = toFloatArray(resources.obtainTypedArray(
com.android.internal.R.array.config_screenBrightnessNits));
// The last value of this range corresponds to the high-brightness mode.
- float[] nitsAutoBrightnessValues = toFloatArray(context.getResources().obtainTypedArray(
+ float[] nitsAutoBrightnessValues = toFloatArray(resources.obtainTypedArray(
com.android.internal.R.array.config_autoBrightnessDisplayValuesNits));
mHbmNits = nitsAutoBrightnessValues[nitsAutoBrightnessValues.length - 1];
@@ -170,12 +184,12 @@
// This range only consists of the minimum and maximum backlight values, which only apply
// in non-high-brightness mode.
float[] normalizedBacklightRange = normalizeBacklightRange(
- context.getResources().getIntArray(
+ resources.getIntArray(
com.android.internal.R.array.config_screenBrightnessBacklight));
mBacklightToNitsSpline = Spline.createSpline(normalizedBacklightRange, nitsRange);
mNitsToHbmBacklightSpline = Spline.createSpline(hbmNitsRange, normalizedBacklightRange);
- mDefaultBrightness = obtainDefaultBrightness(context);
+ mDefaultBrightness = obtainDefaultBrightness(powerManager);
// TODO(b/160025856): move to the "dump" method.
Log.v(TAG, String.format("ctor | mNitsRange: [%f, %f]", nitsRange[0], nitsRange[1]));
@@ -194,7 +208,7 @@
}
private void showUdfpsOverlay() {
- mHandler.post(() -> {
+ mFgExecutor.execute(() -> {
if (!mIsOverlayShowing) {
try {
Log.v(TAG, "showUdfpsOverlay | adding window");
@@ -211,7 +225,7 @@
}
private void hideUdfpsOverlay() {
- mHandler.post(() -> {
+ mFgExecutor.execute(() -> {
if (mIsOverlayShowing) {
Log.v(TAG, "hideUdfpsOverlay | removing window");
mView.setOnTouchListener(null);
@@ -228,7 +242,7 @@
// Returns a value in the range of [0, 255].
private int computeScrimOpacity() {
// Backlight setting can be NaN, -1.0f, and [0.0f, 1.0f].
- float backlightSetting = Settings.System.getFloatForUser(mContentResolver,
+ float backlightSetting = mSystemSettings.getFloatForUser(
Settings.System.SCREEN_BRIGHTNESS_FLOAT, mDefaultBrightness,
UserHandle.USER_CURRENT);
@@ -265,7 +279,8 @@
// Since the sensor that triggers the AOD interrupt doesn't provide ACTION_UP/ACTION_CANCEL,
// we need to be careful about not letting the screen accidentally remain in high brightness
// mode. As a mitigation, queue a call to cancel the fingerprint scan.
- mHandler.postDelayed(mAodInterruptTimeoutAction, AOD_INTERRUPT_TIMEOUT_MILLIS);
+ mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::onCancelAodInterrupt,
+ AOD_INTERRUPT_TIMEOUT_MILLIS);
// using a hard-coded value for major and minor until it is available from the sensor
onFingerDown(screenX, screenY, 13.0f, 13.0f);
}
@@ -280,7 +295,10 @@
if (!mIsAodInterruptActive) {
return;
}
- mHandler.removeCallbacks(mAodInterruptTimeoutAction);
+ if (mCancelAodTimeoutAction != null) {
+ mCancelAodTimeoutAction.run();
+ mCancelAodTimeoutAction = null;
+ }
mIsAodInterruptActive = false;
onFingerUp();
}
@@ -338,8 +356,7 @@
return lp;
}
- private static float obtainDefaultBrightness(Context context) {
- PowerManager powerManager = context.getSystemService(PowerManager.class);
+ private static float obtainDefaultBrightness(PowerManager powerManager) {
if (powerManager == null) {
Log.e(TAG, "PowerManager is unavailable. Can't obtain default brightness.");
return 0f;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
index db77e08..73c6504 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
@@ -80,8 +80,6 @@
mFinished = false;
// Enqueue jobs to fetch every system tile and then ever package tile.
addCurrentAndStockTiles(host);
-
- addPackageTiles(host);
}
public boolean isFinished() {
@@ -122,23 +120,86 @@
tile.destroy();
continue;
}
- tile.setListening(this, true);
- tile.refreshState();
- tile.setListening(this, false);
tile.setTileSpec(spec);
tilesToAdd.add(tile);
}
- mBgExecutor.execute(() -> {
- for (QSTile tile : tilesToAdd) {
- final QSTile.State state = tile.getState().copy();
- // Ignore the current state and get the generic label instead.
- state.label = tile.getTileLabel();
- tile.destroy();
- addTile(tile.getTileSpec(), null, state, true);
+ new TileCollector(tilesToAdd, host).startListening();
+ }
+
+ private static class TilePair {
+ QSTile mTile;
+ boolean mReady = false;
+ }
+
+ private class TileCollector implements QSTile.Callback {
+
+ private final List<TilePair> mQSTileList = new ArrayList<>();
+ private final QSTileHost mQSTileHost;
+
+ TileCollector(List<QSTile> tilesToAdd, QSTileHost host) {
+ for (QSTile tile: tilesToAdd) {
+ TilePair pair = new TilePair();
+ pair.mTile = tile;
+ mQSTileList.add(pair);
}
+ mQSTileHost = host;
+ if (tilesToAdd.isEmpty()) {
+ mBgExecutor.execute(this::finished);
+ }
+ }
+
+ private void finished() {
notifyTilesChanged(false);
- });
+ addPackageTiles(mQSTileHost);
+ }
+
+ private void startListening() {
+ for (TilePair pair: mQSTileList) {
+ pair.mTile.addCallback(this);
+ pair.mTile.setListening(this, true);
+ // Make sure that at least one refresh state happens
+ pair.mTile.refreshState();
+ }
+ }
+
+ // This is called in the Bg thread
+ @Override
+ public void onStateChanged(State s) {
+ boolean allReady = true;
+ for (TilePair pair: mQSTileList) {
+ if (!pair.mReady && pair.mTile.isTileReady()) {
+ pair.mTile.removeCallback(this);
+ pair.mTile.setListening(this, false);
+ pair.mReady = true;
+ } else if (!pair.mReady) {
+ allReady = false;
+ }
+ }
+ if (allReady) {
+ for (TilePair pair : mQSTileList) {
+ QSTile tile = pair.mTile;
+ final QSTile.State state = tile.getState().copy();
+ // Ignore the current state and get the generic label instead.
+ state.label = tile.getTileLabel();
+ tile.destroy();
+ addTile(tile.getTileSpec(), null, state, true);
+ }
+ finished();
+ }
+ }
+
+ @Override
+ public void onShowDetail(boolean show) {}
+
+ @Override
+ public void onToggleStateChanged(boolean state) {}
+
+ @Override
+ public void onScanStateChanged(boolean state) {}
+
+ @Override
+ public void onAnnouncementRequested(CharSequence announcement) {}
}
private void addPackageTiles(final QSTileHost host) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
index 255513a..dfd7e2c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -90,6 +90,10 @@
private static final long DEFAULT_STALE_TIMEOUT = 10 * DateUtils.MINUTE_IN_MILLIS;
protected static final Object ARG_SHOW_TRANSIENT_ENABLING = new Object();
+ private static final int READY_STATE_NOT_READY = 0;
+ private static final int READY_STATE_READYING = 1;
+ private static final int READY_STATE_READY = 2;
+
protected final QSHost mHost;
protected final Context mContext;
// @NonFinalForTesting
@@ -101,6 +105,7 @@
protected final ActivityStarter mActivityStarter;
private final UiEventLogger mUiEventLogger;
private final QSLogger mQSLogger;
+ private volatile int mReadyState;
private final ArrayList<Callback> mCallbacks = new ArrayList<>();
private final Object mStaleListener = new Object();
@@ -386,7 +391,11 @@
protected void handleRefreshState(Object arg) {
handleUpdateState(mTmpState, arg);
- final boolean changed = mTmpState.copyTo(mState);
+ boolean changed = mTmpState.copyTo(mState);
+ if (mReadyState == READY_STATE_READYING) {
+ mReadyState = READY_STATE_READY;
+ changed = true;
+ }
if (changed) {
mQSLogger.logTileUpdated(mTileSpec, mState);
handleStateChanged();
@@ -459,6 +468,9 @@
// should not refresh it anymore.
if (mLifecycle.getCurrentState().equals(DESTROYED)) return;
mLifecycle.setCurrentState(RESUMED);
+ if (mReadyState == READY_STATE_NOT_READY) {
+ mReadyState = READY_STATE_READYING;
+ }
refreshState(); // Ensure we get at least one refresh after listening.
});
}
@@ -531,6 +543,15 @@
*/
public abstract CharSequence getTileLabel();
+ /**
+ * @return {@code true} if the tile has refreshed state at least once after having set its
+ * lifecycle to {@link Lifecycle.State#RESUMED}.
+ */
+ @Override
+ public boolean isTileReady() {
+ return mReadyState == READY_STATE_READY;
+ }
+
public static int getColorForState(Context context, int state) {
switch (state) {
case Tile.STATE_UNAVAILABLE:
diff --git a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java
index 8f082c1..ade3290 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java
@@ -47,6 +47,7 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dump.DumpManager;
import org.junit.Before;
@@ -82,6 +83,8 @@
private PackageManager mPackageManager;
@Mock(stubOnly = true)
private AudioManager mAudioManager;
+ @Mock()
+ private BroadcastDispatcher mDispatcher;
@Mock(stubOnly = true)
private AudioManager.AudioRecordingCallback mRecordingCallback;
@Mock(stubOnly = true)
@@ -120,7 +123,8 @@
mTestableLooper.getLooper(),
mDumpManager,
mFlagsCache,
- mAudioManager
+ mAudioManager,
+ mDispatcher
);
}
@@ -128,12 +132,14 @@
public void testOnlyListenForFewOps() {
mController.setListening(true);
verify(mAppOpsManager, times(1)).startWatchingActive(AppOpsControllerImpl.OPS, mController);
+ verify(mDispatcher, times(1)).registerReceiverWithHandler(eq(mController), any(), any());
}
@Test
public void testStopListening() {
mController.setListening(false);
verify(mAppOpsManager, times(1)).stopWatchingActive(mController);
+ verify(mDispatcher, times(1)).unregisterReceiver(mController);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
new file mode 100644
index 0000000..87ec72f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.IUdfpsOverlayController;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper.RunWithLooper;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.settings.FakeSettings;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper
+public class UdfpsControllerTest extends SysuiTestCase {
+
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
+
+ // Unit under test
+ private UdfpsController mUdfpsController;
+
+ // Dependencies
+ @Mock
+ private Resources mResources;
+ @Mock
+ private LayoutInflater mLayoutInflater;
+ @Mock
+ private FingerprintManager mFingerprintManager;
+ @Mock
+ private PowerManager mPowerManager;
+ @Mock
+ private WindowManager mWindowManager;
+ @Mock
+ private StatusBarStateController mStatusBarStateController;
+ private FakeSettings mSystemSettings;
+ private FakeExecutor mFgExecutor;
+
+ // Stuff for configuring mocks
+ @Mock
+ private UdfpsView mUdfpsView;
+ @Mock
+ private TypedArray mBrightnessValues;
+ @Mock
+ private TypedArray mBrightnessBacklight;
+
+ // Capture listeners so that they can be used to send events
+ @Captor private ArgumentCaptor<IUdfpsOverlayController> mOverlayCaptor;
+ private IUdfpsOverlayController mOverlayController;
+ @Captor private ArgumentCaptor<UdfpsView.OnTouchListener> mTouchListenerCaptor;
+
+ @Before
+ public void setUp() {
+ setUpResources();
+ when(mLayoutInflater.inflate(R.layout.udfps_view, null, false)).thenReturn(mUdfpsView);
+ mSystemSettings = new FakeSettings();
+ mFgExecutor = new FakeExecutor(new FakeSystemClock());
+ mUdfpsController = new UdfpsController(
+ mContext,
+ mResources,
+ mLayoutInflater,
+ mFingerprintManager,
+ mPowerManager,
+ mWindowManager,
+ mSystemSettings,
+ mStatusBarStateController,
+ mFgExecutor);
+ verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
+ mOverlayController = mOverlayCaptor.getValue();
+ }
+
+ private void setUpResources() {
+ when(mBrightnessValues.length()).thenReturn(2);
+ when(mBrightnessValues.getFloat(0, PowerManager.BRIGHTNESS_OFF_FLOAT)).thenReturn(1f);
+ when(mBrightnessValues.getFloat(1, PowerManager.BRIGHTNESS_OFF_FLOAT)).thenReturn(2f);
+ when(mResources.obtainTypedArray(com.android.internal.R.array.config_screenBrightnessNits))
+ .thenReturn(mBrightnessValues);
+ when(mBrightnessBacklight.length()).thenReturn(2);
+ when(mBrightnessBacklight.getFloat(0, PowerManager.BRIGHTNESS_OFF_FLOAT)).thenReturn(1f);
+ when(mBrightnessBacklight.getFloat(1, PowerManager.BRIGHTNESS_OFF_FLOAT)).thenReturn(2f);
+ when(mResources.obtainTypedArray(
+ com.android.internal.R.array.config_autoBrightnessDisplayValuesNits))
+ .thenReturn(mBrightnessBacklight);
+ when(mResources.getIntArray(com.android.internal.R.array.config_screenBrightnessBacklight))
+ .thenReturn(new int[]{1, 2});
+ }
+
+ @Test
+ public void dozeTimeTick() {
+ mUdfpsController.dozeTimeTick();
+ verify(mUdfpsView).dozeTimeTick();
+ }
+
+ @Test
+ public void showUdfpsOverlay_addsViewToWindow() throws RemoteException {
+ mOverlayController.showUdfpsOverlay();
+ mFgExecutor.runAllReady();
+ verify(mWindowManager).addView(eq(mUdfpsView), any());
+ }
+
+ @Test
+ public void hideUdfpsOverlay_removesViewFromWindow() throws RemoteException {
+ mOverlayController.showUdfpsOverlay();
+ mOverlayController.hideUdfpsOverlay();
+ mFgExecutor.runAllReady();
+ verify(mWindowManager).removeView(eq(mUdfpsView));
+ }
+
+ @Test
+ public void fingerDown() throws RemoteException {
+ // Configure UdfpsView to accept the ACTION_DOWN event
+ when(mUdfpsView.isScrimShowing()).thenReturn(false);
+ when(mUdfpsView.isValidTouch(anyFloat(), anyFloat(), anyFloat())).thenReturn(true);
+
+ // GIVEN that the overlay is showing
+ mOverlayController.showUdfpsOverlay();
+ mFgExecutor.runAllReady();
+ // WHEN ACTION_DOWN is received
+ verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
+ MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
+ mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
+ event.recycle();
+ // THEN the event is passed to the FingerprintManager
+ verify(mFingerprintManager).onFingerDown(eq(0), eq(0), eq(0f), eq(0f));
+ // AND the scrim and dot is shown
+ verify(mUdfpsView).showScrimAndDot();
+ }
+
+ @Test
+ public void aodInterrupt() throws RemoteException {
+ // GIVEN that the overlay is showing
+ mOverlayController.showUdfpsOverlay();
+ mFgExecutor.runAllReady();
+ // WHEN fingerprint is requested because of AOD interrupt
+ mUdfpsController.onAodInterrupt(0, 0);
+ // THEN the event is passed to the FingerprintManager
+ verify(mFingerprintManager).onFingerDown(eq(0), eq(0), anyFloat(), anyFloat());
+ // AND the scrim and dot is shown
+ verify(mUdfpsView).showScrimAndDot();
+ }
+
+ @Test
+ public void cancelAodInterrupt() throws RemoteException {
+ // GIVEN AOD interrupt
+ mOverlayController.showUdfpsOverlay();
+ mFgExecutor.runAllReady();
+ mUdfpsController.onAodInterrupt(0, 0);
+ // WHEN it is cancelled
+ mUdfpsController.onCancelAodInterrupt();
+ // THEN the scrim and dot is hidden
+ verify(mUdfpsView).hideScrimAndDot();
+ }
+
+ @Test
+ public void aodInterruptTimeout() throws RemoteException {
+ // GIVEN AOD interrupt
+ mOverlayController.showUdfpsOverlay();
+ mFgExecutor.runAllReady();
+ mUdfpsController.onAodInterrupt(0, 0);
+ // WHEN it times out
+ mFgExecutor.advanceClockToNext();
+ mFgExecutor.runAllReady();
+ // THEN the scrim and dot is hidden
+ verify(mUdfpsView).hideScrimAndDot();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
index 53ef866..2ef7c65 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
@@ -34,6 +34,7 @@
import static org.mockito.Mockito.when;
import android.Manifest;
+import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
@@ -47,8 +48,11 @@
import androidx.test.filters.SmallTest;
+import com.android.internal.logging.InstanceId;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.qs.DetailAdapter;
+import com.android.systemui.plugins.qs.QSIconView;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.qs.QSTileHost;
import com.android.systemui.util.concurrency.FakeExecutor;
@@ -68,6 +72,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.Executor;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -116,11 +121,9 @@
doAnswer(invocation -> {
String spec = (String) invocation.getArguments()[0];
if (FACTORY_TILES.contains(spec)) {
- QSTile m = mock(QSTile.class);
- when(m.isAvailable()).thenReturn(true);
- when(m.getTileSpec()).thenReturn(spec);
- when(m.getState()).thenReturn(mState);
- return m;
+ FakeQSTile tile = new FakeQSTile(mBgExecutor, mMainExecutor);
+ tile.setState(mState);
+ return tile;
} else {
return null;
}
@@ -292,4 +295,132 @@
verifier.verify(t).setTileSpec("hotspot");
verifier.verify(t).destroy();
}
+
+ private static class FakeQSTile implements QSTile {
+
+ private String mSpec = "";
+ private List<Callback> mCallbacks = new ArrayList<>();
+ private boolean mRefreshed;
+ private boolean mListening;
+ private State mState = new State();
+ private final Executor mBgExecutor;
+ private final Executor mMainExecutor;
+
+ FakeQSTile(Executor bgExecutor, Executor mainExecutor) {
+ mBgExecutor = bgExecutor;
+ mMainExecutor = mainExecutor;
+ }
+
+ @Override
+ public String getTileSpec() {
+ return mSpec;
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public void setTileSpec(String tileSpec) {
+ mSpec = tileSpec;
+ }
+
+ public void setState(State state) {
+ mState = state;
+ notifyChangedState(mState);
+ }
+
+ @Override
+ public void refreshState() {
+ mBgExecutor.execute(() -> {
+ mRefreshed = true;
+ notifyChangedState(mState);
+ });
+ }
+
+ private void notifyChangedState(State state) {
+ List<Callback> callbacks = new ArrayList<>(mCallbacks);
+ callbacks.forEach(callback -> callback.onStateChanged(state));
+ }
+
+ @Override
+ public void addCallback(Callback callback) {
+ mCallbacks.add(callback);
+ }
+
+ @Override
+ public void removeCallback(Callback callback) {
+ mCallbacks.remove(callback);
+ }
+
+ @Override
+ public void removeCallbacks() {
+ mCallbacks.clear();
+ }
+
+ @Override
+ public void setListening(Object client, boolean listening) {
+ if (listening) {
+ mMainExecutor.execute(() -> {
+ mListening = true;
+ refreshState();
+ });
+ }
+ }
+
+ @Override
+ public CharSequence getTileLabel() {
+ return mSpec;
+ }
+
+ @Override
+ public State getState() {
+ return mState;
+ }
+
+ @Override
+ public boolean isTileReady() {
+ return mListening && mRefreshed;
+ }
+
+ @Override
+ public QSIconView createTileView(Context context) {
+ return null;
+ }
+
+ @Override
+ public void click() {}
+
+ @Override
+ public void secondaryClick() {}
+
+ @Override
+ public void longClick() {}
+
+ @Override
+ public void userSwitch(int currentUser) {}
+
+ @Override
+ public int getMetricsCategory() {
+ return 0;
+ }
+
+ @Override
+ public InstanceId getInstanceId() {
+ return null;
+ }
+
+ @Override
+ public void setDetailListening(boolean show) {}
+
+ @Override
+ public void destroy() {}
+
+
+ @Override
+ public DetailAdapter getDetailAdapter() {
+ return null;
+ }
+ }
}
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 9660389..8112bb8 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -22,6 +22,7 @@
import android.app.ActivityManager;
import android.app.ActivityThread;
+import android.app.ApplicationExitInfo;
import android.os.Debug;
import android.os.Handler;
import android.os.Message;
@@ -132,11 +133,15 @@
static final int REPORT_UNFREEZE_MSG = 4;
//TODO:change this static definition into a configurable flag.
- static final int FREEZE_TIMEOUT_MS = 500;
+ static final int FREEZE_TIMEOUT_MS = 10000;
static final int DO_FREEZE = 1;
static final int REPORT_UNFREEZE = 2;
+ // Bitfield values for sync/async transactions reveived by frozen processes
+ static final int SYNC_RECEIVED_WHILE_FROZEN = 1;
+ static final int ASYNC_RECEIVED_WHILE_FROZEN = 2;
+
/**
* This thread must be moved to the system background cpuset.
* If that doesn't happen, it's probably going to draw a lot of power.
@@ -494,6 +499,15 @@
private static native void freezeBinder(int pid, boolean freeze);
/**
+ * Retrieves binder freeze info about a process.
+ * @param pid the pid for which binder freeze info is to be retrieved.
+ *
+ * @throws RuntimeException if the operation could not complete successfully.
+ * @return a bit field reporting the binder freeze info for the process.
+ */
+ private static native int getBinderFreezeInfo(int pid);
+
+ /**
* Determines whether the freezer is supported by this system
*/
public static boolean isFreezerSupported() {
@@ -729,6 +743,37 @@
return;
}
+ boolean processKilled = false;
+
+ try {
+ int freezeInfo = getBinderFreezeInfo(app.pid);
+
+ if ((freezeInfo & SYNC_RECEIVED_WHILE_FROZEN) != 0) {
+ Slog.d(TAG_AM, "pid " + app.pid + " " + app.processName + " "
+ + " received sync transactions while frozen, killing");
+ app.kill("Sync transaction while in frozen state",
+ ApplicationExitInfo.REASON_OTHER,
+ ApplicationExitInfo.SUBREASON_INVALID_STATE, true);
+ processKilled = true;
+ }
+
+ if ((freezeInfo & ASYNC_RECEIVED_WHILE_FROZEN) != 0) {
+ Slog.d(TAG_AM, "pid " + app.pid + " " + app.processName + " "
+ + " received async transactions while frozen");
+ }
+ } catch (Exception e) {
+ Slog.d(TAG_AM, "Unable to query binder frozen info for pid " + app.pid + " "
+ + app.processName + ". Killing it. Exception: " + e);
+ app.kill("Unable to query binder frozen stats",
+ ApplicationExitInfo.REASON_OTHER,
+ ApplicationExitInfo.SUBREASON_INVALID_STATE, true);
+ processKilled = true;
+ }
+
+ if (processKilled) {
+ return;
+ }
+
long freezeTime = app.freezeUnfreezeTime;
try {
@@ -745,8 +790,12 @@
try {
freezeBinder(app.pid, false);
} catch (RuntimeException e) {
- // TODO: it might be preferable to kill the target pid in this case
- Slog.e(TAG_AM, "Unable to unfreeze binder for " + app.pid + " " + app.processName);
+ Slog.e(TAG_AM, "Unable to unfreeze binder for " + app.pid + " " + app.processName
+ + ". Killing it");
+ app.kill("Unable to unfreeze",
+ ApplicationExitInfo.REASON_OTHER,
+ ApplicationExitInfo.SUBREASON_INVALID_STATE, true);
+ return;
}
if (DEBUG_FREEZER) {
diff --git a/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java b/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java
new file mode 100644
index 0000000..92dabe3
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.timezone;
+
+import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DISABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.location.timezone.LocationTimeZoneEvent;
+import android.util.IndentingPrintWriter;
+import android.util.Slog;
+
+import com.android.internal.location.timezone.LocationTimeZoneProviderRequest;
+
+import java.util.Objects;
+
+/**
+ * The real, system-server side implementation of a binder call backed {@link
+ * LocationTimeZoneProvider}. It handles keeping track of current state, timeouts and ensuring
+ * events are passed to the {@link LocationTimeZoneProviderController} on the required thread.
+ */
+class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider {
+
+ private static final String TAG = LocationTimeZoneManagerService.TAG;
+
+ @NonNull private final LocationTimeZoneProviderProxy mProxy;
+
+ BinderLocationTimeZoneProvider(
+ @NonNull ThreadingDomain threadingDomain,
+ @NonNull String providerName,
+ @NonNull LocationTimeZoneProviderProxy proxy) {
+ super(threadingDomain, providerName);
+ mProxy = Objects.requireNonNull(proxy);
+ }
+
+ @Override
+ void onInitialize() {
+ mProxy.setListener(new LocationTimeZoneProviderProxy.Listener() {
+ @Override
+ public void onReportLocationTimeZoneEvent(
+ @NonNull LocationTimeZoneEvent locationTimeZoneEvent) {
+ handleLocationTimeZoneEvent(locationTimeZoneEvent);
+ }
+
+ @Override
+ public void onProviderBound() {
+ handleOnProviderBound();
+ }
+
+ @Override
+ public void onProviderUnbound() {
+ handleProviderLost("onProviderUnbound()");
+ }
+ });
+ }
+
+ private void handleProviderLost(String reason) {
+ mThreadingDomain.assertCurrentThread();
+
+ synchronized (mSharedLock) {
+ ProviderState currentState = mCurrentState.get();
+ switch (currentState.stateEnum) {
+ case PROVIDER_STATE_ENABLED: {
+ // Losing a remote provider is treated as becoming uncertain.
+ String msg = "handleProviderLost reason=" + reason
+ + ", mProviderName=" + mProviderName
+ + ", currentState=" + currentState;
+ debugLog(msg);
+ // This is an unusual PROVIDER_STATE_ENABLED state because event == null
+ ProviderState newState = currentState.newState(
+ PROVIDER_STATE_ENABLED, null, currentState.currentUserConfiguration,
+ msg);
+ setCurrentState(newState, true);
+ break;
+ }
+ case PROVIDER_STATE_DISABLED: {
+ debugLog("handleProviderLost reason=" + reason
+ + ", mProviderName=" + mProviderName
+ + ", currentState=" + currentState
+ + ": No state change required, provider is disabled.");
+ break;
+ }
+ case PROVIDER_STATE_PERM_FAILED: {
+ debugLog("handleProviderLost reason=" + reason
+ + ", mProviderName=" + mProviderName
+ + ", currentState=" + currentState
+ + ": No state change required, provider is perm failed.");
+ break;
+ }
+ default: {
+ throw new IllegalStateException("Unknown currentState=" + currentState);
+ }
+ }
+ }
+ }
+
+ private void handleOnProviderBound() {
+ mThreadingDomain.assertCurrentThread();
+
+ synchronized (mSharedLock) {
+ ProviderState currentState = mCurrentState.get();
+ switch (currentState.stateEnum) {
+ case PROVIDER_STATE_ENABLED: {
+ debugLog("handleOnProviderBound mProviderName=" + mProviderName
+ + ", currentState=" + currentState + ": Provider is enabled.");
+ break;
+ }
+ case PROVIDER_STATE_DISABLED: {
+ debugLog("handleOnProviderBound mProviderName=" + mProviderName
+ + ", currentState=" + currentState + ": Provider is disabled.");
+ break;
+ }
+ case PROVIDER_STATE_PERM_FAILED: {
+ debugLog("handleOnProviderBound"
+ + ", mProviderName=" + mProviderName
+ + ", currentState=" + currentState
+ + ": No state change required, provider is perm failed.");
+ break;
+ }
+ default: {
+ throw new IllegalStateException("Unknown currentState=" + currentState);
+ }
+ }
+ }
+ }
+
+ @Override
+ void onEnable() {
+ // Set a request on the proxy - it will be sent immediately if the service is bound,
+ // or will be sent as soon as the service becomes bound.
+ // TODO(b/152744911): Decide whether to send a timeout so the provider knows how long
+ // it has to generate the first event before it could be bypassed.
+ LocationTimeZoneProviderRequest request =
+ new LocationTimeZoneProviderRequest.Builder()
+ .setReportLocationTimeZone(true)
+ .build();
+ mProxy.setRequest(request);
+ }
+
+ @Override
+ void onDisable() {
+ LocationTimeZoneProviderRequest request =
+ new LocationTimeZoneProviderRequest.Builder()
+ .setReportLocationTimeZone(false)
+ .build();
+ mProxy.setRequest(request);
+ }
+
+ @Override
+ void logWarn(String msg) {
+ Slog.w(TAG, msg);
+ }
+
+ @Override
+ public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
+ synchronized (mSharedLock) {
+ ipw.println("{BinderLocationTimeZoneProvider}");
+ ipw.println("mProviderName=" + mProviderName);
+ ipw.println("mCurrentState=" + mCurrentState);
+ ipw.println("mProxy=" + mProxy);
+
+ ipw.println("State history:");
+ ipw.increaseIndent();
+ mCurrentState.dump(ipw);
+ ipw.decreaseIndent();
+
+ ipw.println("Proxy details:");
+ ipw.increaseIndent();
+ mProxy.dump(ipw, args);
+ ipw.decreaseIndent();
+ }
+ }
+
+ @Override
+ public String toString() {
+ synchronized (mSharedLock) {
+ return "BinderLocationTimeZoneProvider{"
+ + "mProviderName=" + mProviderName
+ + "mCurrentState=" + mCurrentState
+ + "mProxy=" + mProxy
+ + '}';
+ }
+ }
+
+ /**
+ * Passes the supplied simulation / testing event to the current proxy iff the proxy is a
+ * {@link SimulatedLocationTimeZoneProviderProxy}. If not, the event is logged but discarded.
+ */
+ void simulateBinderProviderEvent(SimulatedBinderProviderEvent event) {
+ if (!(mProxy instanceof SimulatedLocationTimeZoneProviderProxy)) {
+ Slog.w(TAG, mProxy + " is not a " + SimulatedLocationTimeZoneProviderProxy.class
+ + ", event=" + event);
+ return;
+ }
+ ((SimulatedLocationTimeZoneProviderProxy) mProxy).simulate(event);
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/ControllerCallbackImpl.java b/services/core/java/com/android/server/location/timezone/ControllerCallbackImpl.java
new file mode 100644
index 0000000..cd9aa2f
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/ControllerCallbackImpl.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.timezone;
+
+import android.annotation.NonNull;
+
+import com.android.server.LocalServices;
+import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion;
+import com.android.server.timezonedetector.TimeZoneDetectorInternal;
+
+/**
+ * The real implementation of {@link LocationTimeZoneProviderController.Callback} used by
+ * {@link ControllerImpl} to interact with other server components.
+ */
+class ControllerCallbackImpl extends LocationTimeZoneProviderController.Callback {
+
+ ControllerCallbackImpl(@NonNull ThreadingDomain threadingDomain) {
+ super(threadingDomain);
+ }
+
+ @Override
+ void suggest(@NonNull GeolocationTimeZoneSuggestion suggestion) {
+ mThreadingDomain.assertCurrentThread();
+
+ TimeZoneDetectorInternal timeZoneDetector =
+ LocalServices.getService(TimeZoneDetectorInternal.class);
+ timeZoneDetector.suggestGeolocationTimeZone(suggestion);
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/ControllerEnvironmentImpl.java b/services/core/java/com/android/server/location/timezone/ControllerEnvironmentImpl.java
new file mode 100644
index 0000000..2e2481c
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/ControllerEnvironmentImpl.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.timezone;
+
+import android.annotation.NonNull;
+
+import com.android.server.LocalServices;
+import com.android.server.timezonedetector.ConfigurationInternal;
+import com.android.server.timezonedetector.TimeZoneDetectorInternal;
+
+import java.util.Objects;
+
+/**
+ * The real implementation of {@link LocationTimeZoneProviderController.Environment} used by
+ * {@link ControllerImpl} to interact with other server components.
+ */
+class ControllerEnvironmentImpl extends LocationTimeZoneProviderController.Environment {
+
+ @NonNull private final TimeZoneDetectorInternal mTimeZoneDetectorInternal;
+ @NonNull private final LocationTimeZoneProviderController mController;
+
+ ControllerEnvironmentImpl(@NonNull ThreadingDomain threadingDomain,
+ @NonNull LocationTimeZoneProviderController controller) {
+ super(threadingDomain);
+ mController = Objects.requireNonNull(controller);
+ mTimeZoneDetectorInternal = LocalServices.getService(TimeZoneDetectorInternal.class);
+
+ // Listen for configuration changes.
+ mTimeZoneDetectorInternal.addConfigurationListener(
+ () -> mThreadingDomain.post(mController::onConfigChanged));
+ }
+
+ @Override
+ ConfigurationInternal getCurrentUserConfigurationInternal() {
+ return mTimeZoneDetectorInternal.getCurrentUserConfigurationInternal();
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/ControllerImpl.java b/services/core/java/com/android/server/location/timezone/ControllerImpl.java
new file mode 100644
index 0000000..e31cfc4
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/ControllerImpl.java
@@ -0,0 +1,420 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.timezone;
+
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_PERMANENT_FAILURE;
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_SUCCESS;
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_UNCERTAIN;
+
+import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog;
+import static com.android.server.location.timezone.LocationTimeZoneManagerService.warnLog;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DISABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.location.timezone.LocationTimeZoneEvent;
+import android.util.IndentingPrintWriter;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.location.timezone.ThreadingDomain.SingleRunnableQueue;
+import com.android.server.timezonedetector.ConfigurationInternal;
+import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion;
+
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * A real implementation of {@link LocationTimeZoneProviderController} that supports a single
+ * {@link LocationTimeZoneProvider}.
+ *
+ * TODO(b/152744911): This implementation currently only supports a single ("primary") provider.
+ * Support for a secondary provider will be added in a later commit.
+ */
+class ControllerImpl extends LocationTimeZoneProviderController {
+
+ @VisibleForTesting
+ static final Duration UNCERTAINTY_DELAY = Duration.ofMinutes(5);
+
+ @NonNull private final LocationTimeZoneProvider mProvider;
+ @NonNull private final SingleRunnableQueue mDelayedSuggestionQueue;
+
+ @GuardedBy("mSharedLock")
+ // Non-null after initialize()
+ private ConfigurationInternal mCurrentUserConfiguration;
+
+ @GuardedBy("mSharedLock")
+ // Non-null after initialize()
+ private Environment mEnvironment;
+
+ @GuardedBy("mSharedLock")
+ // Non-null after initialize()
+ private Callback mCallback;
+
+ /**
+ * Contains any currently pending suggestion on {@link #mDelayedSuggestionQueue}, if there is
+ * one.
+ */
+ @GuardedBy("mSharedLock")
+ @Nullable
+ private GeolocationTimeZoneSuggestion mPendingSuggestion;
+
+ /** Contains the last suggestion actually made, if there is one. */
+ @GuardedBy("mSharedLock")
+ @Nullable
+ private GeolocationTimeZoneSuggestion mLastSuggestion;
+
+ ControllerImpl(@NonNull ThreadingDomain threadingDomain,
+ @NonNull LocationTimeZoneProvider provider) {
+ super(threadingDomain);
+ mDelayedSuggestionQueue = threadingDomain.createSingleRunnableQueue();
+ mProvider = Objects.requireNonNull(provider);
+ }
+
+ @Override
+ void initialize(@NonNull Environment environment, @NonNull Callback callback) {
+ mThreadingDomain.assertCurrentThread();
+
+ synchronized (mSharedLock) {
+ debugLog("initialize()");
+ mEnvironment = Objects.requireNonNull(environment);
+ mCallback = Objects.requireNonNull(callback);
+ mCurrentUserConfiguration = environment.getCurrentUserConfigurationInternal();
+
+ mProvider.initialize(ControllerImpl.this::onProviderStateChange);
+ enableOrDisableProvider(mCurrentUserConfiguration);
+ }
+ }
+
+ @Override
+ void onConfigChanged() {
+ mThreadingDomain.assertCurrentThread();
+
+ synchronized (mSharedLock) {
+ debugLog("onEnvironmentConfigChanged()");
+
+ ConfigurationInternal oldConfig = mCurrentUserConfiguration;
+ ConfigurationInternal newConfig = mEnvironment.getCurrentUserConfigurationInternal();
+ mCurrentUserConfiguration = newConfig;
+
+ if (!newConfig.equals(oldConfig)) {
+ if (newConfig.getUserId() != oldConfig.getUserId()) {
+ // If the user changed, disable the provider if needed. It may be re-enabled for
+ // the new user below if their settings allow.
+ debugLog("User changed. old=" + oldConfig.getUserId()
+ + ", new=" + newConfig.getUserId());
+ debugLog("Disabling LocationTimeZoneProviders as needed");
+ if (mProvider.getCurrentState().stateEnum == PROVIDER_STATE_ENABLED) {
+ mProvider.disable();
+ }
+ }
+
+ enableOrDisableProvider(newConfig);
+ }
+ }
+ }
+
+ @GuardedBy("mSharedLock")
+ private void enableOrDisableProvider(@NonNull ConfigurationInternal configuration) {
+ ProviderState providerState = mProvider.getCurrentState();
+ boolean geoDetectionEnabled = configuration.getGeoDetectionEnabledBehavior();
+ boolean providerWasEnabled = providerState.stateEnum == PROVIDER_STATE_ENABLED;
+ if (geoDetectionEnabled) {
+ switch (providerState.stateEnum) {
+ case PROVIDER_STATE_DISABLED: {
+ debugLog("Enabling " + mProvider);
+ mProvider.enable(configuration);
+ break;
+ }
+ case PROVIDER_STATE_ENABLED: {
+ debugLog("No need to enable " + mProvider + ": already enabled");
+ break;
+ }
+ case PROVIDER_STATE_PERM_FAILED: {
+ debugLog("Unable to enable " + mProvider + ": it is perm failed");
+ break;
+ }
+ default:
+ warnLog("Unknown provider state: " + mProvider);
+ break;
+ }
+ } else {
+ switch (providerState.stateEnum) {
+ case PROVIDER_STATE_DISABLED: {
+ debugLog("No need to disable " + mProvider + ": already enabled");
+ break;
+ }
+ case PROVIDER_STATE_ENABLED: {
+ debugLog("Disabling " + mProvider);
+ mProvider.disable();
+ break;
+ }
+ case PROVIDER_STATE_PERM_FAILED: {
+ debugLog("Unable to disable " + mProvider + ": it is perm failed");
+ break;
+ }
+ default: {
+ warnLog("Unknown provider state: " + mProvider);
+ break;
+ }
+ }
+ }
+
+ boolean isProviderEnabled =
+ mProvider.getCurrentState().stateEnum == PROVIDER_STATE_ENABLED;
+
+ if (isProviderEnabled) {
+ if (!providerWasEnabled) {
+ // When a provider has first been enabled, we allow it some time for it to
+ // initialize.
+ // This sets up an empty suggestion to trigger if no explicit "certain" or
+ // "uncertain" suggestion preempts it within UNCERTAINTY_DELAY. If, for some reason,
+ // the provider does provide any events then this scheduled suggestion will ensure
+ // the controller makes at least an uncertain suggestion.
+ suggestDelayed(createEmptySuggestion(
+ "No event received in delay=" + UNCERTAINTY_DELAY), UNCERTAINTY_DELAY);
+ }
+ } else {
+ // Clear any queued suggestions.
+ clearDelayedSuggestion();
+
+ // If the provider is now not enabled, and a previous "certain" suggestion has been
+ // made, then a new "uncertain" suggestion must be made to indicate the provider no
+ // longer has an opinion and will not be sending updates.
+ if (mLastSuggestion != null && mLastSuggestion.getZoneIds() != null) {
+ suggestImmediate(createEmptySuggestion(""));
+ }
+ }
+ }
+
+ void onProviderStateChange(@NonNull ProviderState providerState) {
+ mThreadingDomain.assertCurrentThread();
+ assertProviderKnown(providerState.provider);
+
+ synchronized (mSharedLock) {
+ switch (providerState.stateEnum) {
+ case PROVIDER_STATE_DISABLED: {
+ // This should never happen: entering disabled does not trigger an event.
+ warnLog("onProviderStateChange: Unexpected state change for disabled provider,"
+ + " providerState=" + providerState);
+ break;
+ }
+ case PROVIDER_STATE_ENABLED: {
+ // Entering enabled does not trigger an event, so this only happens if an event
+ // is received while the provider is enabled.
+ debugLog("onProviderStateChange: Received notification of an event while"
+ + " enabled, providerState=" + providerState);
+ providerEnabledProcessEvent(providerState);
+ break;
+ }
+ case PROVIDER_STATE_PERM_FAILED: {
+ debugLog("Received notification of permanent failure for"
+ + " provider=" + providerState);
+ GeolocationTimeZoneSuggestion suggestion = createEmptySuggestion(
+ "provider=" + providerState.provider
+ + " permanently failed: " + providerState);
+ suggestImmediate(suggestion);
+ break;
+ }
+ default: {
+ warnLog("onProviderStateChange: Unexpected providerState=" + providerState);
+ }
+ }
+ }
+ }
+
+ private void assertProviderKnown(LocationTimeZoneProvider provider) {
+ if (provider != mProvider) {
+ throw new IllegalArgumentException("Unknown provider: " + provider);
+ }
+ }
+
+ /**
+ * Called when a provider has changed state but just moved from a PROVIDER_STATE_ENABLED state
+ * to another PROVIDER_STATE_ENABLED state, usually as a result of a new {@link
+ * LocationTimeZoneEvent} being received. There are some cases where event can be null.
+ */
+ private void providerEnabledProcessEvent(@NonNull ProviderState providerState) {
+ LocationTimeZoneEvent event = providerState.event;
+ if (event == null) {
+ // Implicit uncertainty, i.e. where the provider is enabled, but a problem has been
+ // detected without having received an event. For example, if the process has detected
+ // the loss of a binder-based provider. This is treated like explicit uncertainty, i.e.
+ // where the provider has explicitly told this process it is uncertain.
+ scheduleUncertainSuggestionIfNeeded(null);
+ return;
+ }
+
+ // Consistency check for user. This may be possible as there are various races around
+ // current user switches.
+ if (!Objects.equals(event.getUserHandle(), mCurrentUserConfiguration.getUserHandle())) {
+ warnLog("Using event=" + event + " from a different user="
+ + mCurrentUserConfiguration);
+ }
+
+ if (!mCurrentUserConfiguration.getGeoDetectionEnabledBehavior()) {
+ // This should not happen: the provider should not be in an enabled state if the user
+ // does not have geodetection enabled.
+ warnLog("Provider=" + providerState + " is enabled, but currentUserConfiguration="
+ + mCurrentUserConfiguration + " suggests it shouldn't be.");
+ }
+
+ switch (event.getEventType()) {
+ case EVENT_TYPE_PERMANENT_FAILURE: {
+ // This shouldn't happen. Providers cannot be enabled and have this event.
+ warnLog("Provider=" + providerState
+ + " is enabled, but event suggests it shouldn't be");
+ break;
+ }
+ case EVENT_TYPE_UNCERTAIN: {
+ scheduleUncertainSuggestionIfNeeded(event);
+ break;
+ }
+ case EVENT_TYPE_SUCCESS: {
+ GeolocationTimeZoneSuggestion suggestion =
+ new GeolocationTimeZoneSuggestion(event.getTimeZoneIds());
+ suggestion.addDebugInfo("Event received provider=" + mProvider.getName()
+ + ", event=" + event);
+ // Rely on the receiver to dedupe events. It is better to over-communicate.
+ suggestImmediate(suggestion);
+ break;
+ }
+ default: {
+ warnLog("Unknown eventType=" + event.getEventType());
+ break;
+ }
+ }
+ }
+
+ /**
+ * Indicates a provider has become uncertain with the event (if any) received that indicates
+ * that.
+ *
+ * <p>Providers are expected to report their uncertainty as soon as they become uncertain, as
+ * this enables the most flexibility for the controller to enable other providers when there are
+ * multiple ones. The controller is therefore responsible for deciding when to make a
+ * "uncertain" suggestion.
+ *
+ * <p>This method schedules an "uncertain" suggestion (if one isn't already scheduled) to be
+ * made later if nothing else preempts it. It can be preempted if the provider becomes certain
+ * (or does anything else that calls {@link #suggestImmediate(GeolocationTimeZoneSuggestion)})
+ * within UNCERTAINTY_DELAY. Preemption causes the scheduled "uncertain" event to be cancelled.
+ * If the provider repeatedly sends uncertainty events within UNCERTAINTY_DELAY, those events
+ * are effectively ignored (i.e. the timer is not reset each time).
+ */
+ private void scheduleUncertainSuggestionIfNeeded(@Nullable LocationTimeZoneEvent event) {
+ if (mPendingSuggestion == null || mPendingSuggestion.getZoneIds() != null) {
+ GeolocationTimeZoneSuggestion suggestion = createEmptySuggestion(
+ "provider=" + mProvider + " became uncertain, event=" + event);
+ suggestDelayed(suggestion, UNCERTAINTY_DELAY);
+ }
+ }
+
+ @Override
+ public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
+ synchronized (mSharedLock) {
+ ipw.println("LocationTimeZoneProviderController:");
+
+ ipw.increaseIndent(); // level 1
+ ipw.println("mCurrentUserConfiguration=" + mCurrentUserConfiguration);
+ ipw.println("mPendingSuggestion=" + mPendingSuggestion);
+ ipw.println("mLastSuggestion=" + mLastSuggestion);
+
+ ipw.println("Provider:");
+ ipw.increaseIndent(); // level 2
+ mProvider.dump(ipw, args);
+ ipw.decreaseIndent(); // level 2
+
+ ipw.decreaseIndent(); // level 1
+ }
+ }
+
+ /** Sends an immediate suggestion, cancelling any pending suggestion. */
+ @GuardedBy("mSharedLock")
+ private void suggestImmediate(@NonNull GeolocationTimeZoneSuggestion suggestion) {
+ debugLog("suggestImmediate: Executing suggestion=" + suggestion);
+ mDelayedSuggestionQueue.runSynchronously(() -> mCallback.suggest(suggestion));
+ mPendingSuggestion = null;
+ mLastSuggestion = suggestion;
+ }
+
+ /** Clears any pending suggestion. */
+ @GuardedBy("mSharedLock")
+ private void clearDelayedSuggestion() {
+ mDelayedSuggestionQueue.cancel();
+ mPendingSuggestion = null;
+ }
+
+
+ /**
+ * Schedules a delayed suggestion. There can only be one delayed suggestion at a time.
+ * If there is a pending scheduled suggestion equal to the one passed, it will not be replaced.
+ * Replacing a previous delayed suggestion has the effect of cancelling the timeout associated
+ * with that previous suggestion.
+ */
+ @GuardedBy("mSharedLock")
+ private void suggestDelayed(@NonNull GeolocationTimeZoneSuggestion suggestion,
+ @NonNull Duration delay) {
+ Objects.requireNonNull(suggestion);
+ Objects.requireNonNull(delay);
+
+ if (Objects.equals(mPendingSuggestion, suggestion)) {
+ // Do not reset the timer.
+ debugLog("suggestDelayed: Suggestion=" + suggestion + " is equal to existing."
+ + " Not scheduled.");
+ return;
+ }
+
+ debugLog("suggestDelayed: Scheduling suggestion=" + suggestion);
+ mPendingSuggestion = suggestion;
+
+ mDelayedSuggestionQueue.runDelayed(() -> {
+ debugLog("suggestDelayed: Executing suggestion=" + suggestion);
+ mCallback.suggest(suggestion);
+ mPendingSuggestion = null;
+ mLastSuggestion = suggestion;
+ }, delay.toMillis());
+ }
+
+ private static GeolocationTimeZoneSuggestion createEmptySuggestion(String reason) {
+ GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(null);
+ suggestion.addDebugInfo(reason);
+ return suggestion;
+ }
+
+ /**
+ * Asynchronously passes a {@link SimulatedBinderProviderEvent] to the appropriate provider.
+ * If the provider name does not match a known provider, then the event is logged and discarded.
+ */
+ void simulateBinderProviderEvent(SimulatedBinderProviderEvent event) {
+ if (!Objects.equals(mProvider.getName(), event.getProviderName())) {
+ warnLog("Unable to process simulated binder provider event,"
+ + " unknown providerName in event=" + event);
+ return;
+ }
+ if (!(mProvider instanceof BinderLocationTimeZoneProvider)) {
+ warnLog("Unable to process simulated binder provider event,"
+ + " provider is not a " + BinderLocationTimeZoneProvider.class
+ + ", event=" + event);
+ return;
+ }
+ ((BinderLocationTimeZoneProvider) mProvider).simulateBinderProviderEvent(event);
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/HandlerThreadingDomain.java b/services/core/java/com/android/server/location/timezone/HandlerThreadingDomain.java
new file mode 100644
index 0000000..17e719e
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/HandlerThreadingDomain.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.timezone;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+
+import java.util.Objects;
+
+/**
+ * The real implementation of {@link ThreadingDomain} that uses a {@link Handler}.
+ */
+final class HandlerThreadingDomain extends ThreadingDomain {
+
+ @NonNull private final Handler mHandler;
+
+ HandlerThreadingDomain(Handler handler) {
+ mHandler = Objects.requireNonNull(handler);
+ }
+
+ /**
+ * Returns the {@link Handler} associated with this threading domain. The same {@link Handler}
+ * may be associated with multiple threading domains, e.g. multiple threading domains could
+ * choose to use the {@link com.android.server.FgThread} handler.
+ *
+ * <p>If you find yourself making this public because you need a {@link Handler}, then it may
+ * cause problems with testability. Try to avoid using this method and use methods like {@link
+ * #post(Runnable)} instead.
+ */
+ @NonNull
+ Handler getHandler() {
+ return mHandler;
+ }
+
+ @NonNull
+ Thread getThread() {
+ return getHandler().getLooper().getThread();
+ }
+
+ @Override
+ void post(@NonNull Runnable r) {
+ getHandler().post(r);
+ }
+
+ @Override
+ void postDelayed(@NonNull Runnable r, long delayMillis) {
+ getHandler().postDelayed(r, delayMillis);
+ }
+
+ @Override
+ void postDelayed(Runnable r, Object token, long delayMillis) {
+ getHandler().postDelayed(r, token, delayMillis);
+ }
+
+ @Override
+ void removeQueuedRunnables(Object token) {
+ getHandler().removeCallbacksAndMessages(token);
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerService.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerService.java
new file mode 100644
index 0000000..238f999
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerService.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.timezone;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Binder;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.os.SystemProperties;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.DumpUtils;
+import com.android.server.FgThread;
+import com.android.server.SystemService;
+import com.android.server.timezonedetector.TimeZoneDetectorInternal;
+import com.android.server.timezonedetector.TimeZoneDetectorService;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Objects;
+
+/**
+ * A service class that acts as a container for the {@link LocationTimeZoneProviderController},
+ * which determines what {@link com.android.server.timezonedetector.GeolocationTimeZoneSuggestion}
+ * are made to the {@link TimeZoneDetectorInternal}, and the {@link LocationTimeZoneProvider}s that
+ * offer {@link android.location.timezone.LocationTimeZoneEvent}s.
+ *
+ * TODO(b/152744911): This implementation currently only supports a primary provider. Support for a
+ * secondary provider must be added in a later commit.
+ *
+ * <p>Implementation details:
+ *
+ * <p>For simplicity, with the exception of a few outliers like {@link #dump}, all processing in
+ * this service (and package-private helper objects) takes place on a single thread / handler, the
+ * one indicated by {@link ThreadingDomain}. Because methods like {@link #dump} can be invoked on
+ * another thread, the service and its related objects must still be thread-safe.
+ *
+ * <p>For testing / reproduction of bugs, it is possible to put providers into "simulation
+ * mode" where the real binder clients are replaced by {@link
+ * SimulatedLocationTimeZoneProviderProxy}. This means that the real client providers are never
+ * bound (ensuring no real location events will be received) and simulated events / behaviors
+ * can be injected via the command line. To enter simulation mode for a provider, use
+ * "{@code adb shell setprop persist.sys.location_tz_simulation_mode.<provider name> 1}" and reboot.
+ * e.g. "{@code adb shell setprop persist.sys.location_tz_simulation_mode.primary 1}}"
+ * Then use "{@code adb shell cmd location_time_zone_manager help}" for injection. Set the system
+ * properties to "0" and reboot to return to exit simulation mode.
+ */
+public class LocationTimeZoneManagerService extends Binder {
+
+ /**
+ * Controls lifecycle of the {@link LocationTimeZoneManagerService}.
+ */
+ public static class Lifecycle extends SystemService {
+
+ private LocationTimeZoneManagerService mService;
+
+ public Lifecycle(@NonNull Context context) {
+ super(Objects.requireNonNull(context));
+ }
+
+ @Override
+ public void onStart() {
+ if (TimeZoneDetectorService.GEOLOCATION_TIME_ZONE_DETECTION_ENABLED) {
+ Context context = getContext();
+ mService = new LocationTimeZoneManagerService(context);
+
+ // The service currently exposes no LocalService or Binder API, but it extends
+ // Binder and is registered as a binder service so it can receive shell commands.
+ publishBinderService("location_time_zone_manager", mService);
+ } else {
+ Slog.i(TAG, getClass() + " is compile-time disabled");
+ }
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (TimeZoneDetectorService.GEOLOCATION_TIME_ZONE_DETECTION_ENABLED) {
+ if (phase == PHASE_SYSTEM_SERVICES_READY) {
+ // The location service must be functioning after this boot phase.
+ mService.onSystemReady();
+ } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
+ // Some providers rely on non-platform code (e.g. gcore), so we wait to
+ // initialize providers until third party code is allowed to run.
+ mService.onSystemThirdPartyAppsCanStart();
+ }
+ }
+ }
+ }
+
+ static final String TAG = "LocationTZDetector";
+
+ static final String PRIMARY_PROVIDER_NAME = "primary";
+
+ private static final String SIMULATION_MODE_SYSTEM_PROPERTY_PREFIX =
+ "persist.sys.location_tz_simulation_mode.";
+
+ private static final String ATTRIBUTION_TAG = "LocationTimeZoneService";
+
+ private static final String PRIMARY_LOCATION_TIME_ZONE_SERVICE_ACTION =
+ "com.android.location.timezone.service.v1.PrimaryLocationTimeZoneProvider";
+
+
+ @NonNull private final Context mContext;
+
+ /**
+ * The {@link ThreadingDomain} used to supply the {@link android.os.Handler} and shared lock
+ * object used by the controller and related components.
+ *
+ * <p>Most operations are executed on the associated handler thread <em>but not all</em>, hence
+ * the requirement for additional synchronization using a shared lock.
+ */
+ @NonNull private final ThreadingDomain mThreadingDomain;
+
+ /** The shared lock from {@link #mThreadingDomain}. */
+ @NonNull private final Object mSharedLock;
+
+ // Lazily initialized. Non-null and effectively final after onSystemThirdPartyAppsCanStart().
+ @GuardedBy("mSharedLock")
+ private ControllerImpl mLocationTimeZoneDetectorController;
+
+ LocationTimeZoneManagerService(Context context) {
+ mContext = context.createAttributionContext(ATTRIBUTION_TAG);
+ mThreadingDomain = new HandlerThreadingDomain(FgThread.getHandler());
+ mSharedLock = mThreadingDomain.getLockObject();
+ }
+
+ void onSystemReady() {
+ // Called on an arbitrary thread during initialization.
+ synchronized (mSharedLock) {
+ // TODO(b/152744911): LocationManagerService watches for packages disappearing. Need to
+ // do anything here?
+
+ // TODO(b/152744911): LocationManagerService watches for foreground app changes. Need to
+ // do anything here?
+ // TODO(b/152744911): LocationManagerService watches screen state. Need to do anything
+ // here?
+ }
+ }
+
+ void onSystemThirdPartyAppsCanStart() {
+ // Called on an arbitrary thread during initialization.
+ synchronized (mSharedLock) {
+ LocationTimeZoneProvider primary = createPrimaryProvider();
+ mLocationTimeZoneDetectorController = new ControllerImpl(mThreadingDomain, primary);
+ ControllerCallbackImpl callback = new ControllerCallbackImpl(mThreadingDomain);
+ ControllerEnvironmentImpl environment = new ControllerEnvironmentImpl(
+ mThreadingDomain, mLocationTimeZoneDetectorController);
+
+ // Initialize the controller on the mThreadingDomain thread: this ensures that the
+ // ThreadingDomain requirements for the controller / environment methods are honored.
+ mThreadingDomain.post(() ->
+ mLocationTimeZoneDetectorController.initialize(environment, callback));
+ }
+ }
+
+ private LocationTimeZoneProvider createPrimaryProvider() {
+ LocationTimeZoneProviderProxy proxy;
+ if (isInSimulationMode(PRIMARY_PROVIDER_NAME)) {
+ proxy = new SimulatedLocationTimeZoneProviderProxy(mContext, mThreadingDomain);
+ } else {
+ // TODO Uncomment this code in a later commit.
+ throw new UnsupportedOperationException("Not implemented");
+ /*
+ proxy = RealLocationTimeZoneProviderProxy.createAndRegister(
+ mContext,
+ mThreadingDomain,
+ PRIMARY_LOCATION_TIME_ZONE_SERVICE_ACTION,
+ com.android.internal.R.bool.config_enablePrimaryLocationTimeZoneOverlay,
+ com.android.internal.R.string.config_primaryLocationTimeZoneProviderPackageName
+ );
+ */
+ }
+ return createLocationTimeZoneProvider(PRIMARY_PROVIDER_NAME, proxy);
+ }
+
+ private boolean isInSimulationMode(String providerName) {
+ return SystemProperties.getBoolean(
+ SIMULATION_MODE_SYSTEM_PROPERTY_PREFIX + providerName, false);
+ }
+
+ private LocationTimeZoneProvider createLocationTimeZoneProvider(
+ @NonNull String providerName, @NonNull LocationTimeZoneProviderProxy proxy) {
+ LocationTimeZoneProvider provider;
+ if (proxy != null) {
+ debugLog("LocationTimeZoneProvider found for providerName=" + providerName);
+ provider = new BinderLocationTimeZoneProvider(mThreadingDomain,
+ providerName, proxy);
+ } else {
+ debugLog("No LocationTimeZoneProvider found for providerName=" + providerName
+ + ": stubbing");
+ provider = new NullLocationTimeZoneProvider(mThreadingDomain, providerName);
+ }
+ return provider;
+ }
+
+ @Override
+ public void onShellCommand(FileDescriptor in, FileDescriptor out,
+ FileDescriptor err, String[] args, ShellCallback callback,
+ ResultReceiver resultReceiver) {
+ (new LocationTimeZoneManagerShellCommand(this)).exec(
+ this, in, out, err, args, callback, resultReceiver);
+ }
+
+ /**
+ * Asynchronously passes a {@link SimulatedBinderProviderEvent] to the appropriate provider.
+ * The device must be in simulation mode, otherwise an {@link IllegalStateException} will be
+ * thrown.
+ */
+ void simulateBinderProviderEvent(SimulatedBinderProviderEvent event)
+ throws IllegalStateException {
+ if (!isInSimulationMode(event.getProviderName())) {
+ throw new IllegalStateException("Use \"setprop "
+ + SIMULATION_MODE_SYSTEM_PROPERTY_PREFIX + event.getProviderName()
+ + " 1\" and reboot before injecting simulated binder events.");
+ }
+ mThreadingDomain.post(
+ () -> mLocationTimeZoneDetectorController.simulateBinderProviderEvent(event));
+ }
+
+ @Override
+ protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw,
+ @Nullable String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
+ // Called on an arbitrary thread at any time.
+ synchronized (mSharedLock) {
+ ipw.println("LocationTimeZoneManagerService:");
+ ipw.increaseIndent();
+ if (mLocationTimeZoneDetectorController == null) {
+ ipw.println("{Uninitialized}");
+ } else {
+ mLocationTimeZoneDetectorController.dump(ipw, args);
+ }
+ ipw.decreaseIndent();
+ }
+ }
+
+ static void debugLog(String msg) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Slog.d(TAG, msg);
+ }
+ }
+
+ static void warnLog(String msg) {
+ if (Log.isLoggable(TAG, Log.WARN)) {
+ Slog.w(TAG, msg);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerShellCommand.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerShellCommand.java
new file mode 100644
index 0000000..7c3b891
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerShellCommand.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.location.timezone;
+
+import android.os.ShellCommand;
+
+import java.io.PrintWriter;
+
+/** Implements the shell command interface for {@link LocationTimeZoneManagerService}. */
+class LocationTimeZoneManagerShellCommand extends ShellCommand {
+
+ private final LocationTimeZoneManagerService mService;
+
+ LocationTimeZoneManagerShellCommand(LocationTimeZoneManagerService service) {
+ mService = service;
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null) {
+ return handleDefaultCommands(cmd);
+ }
+
+ switch (cmd) {
+ case "simulate_binder": {
+ return runSimulateBinderEvent();
+ }
+ default: {
+ return handleDefaultCommands(cmd);
+ }
+ }
+ }
+
+ private int runSimulateBinderEvent() {
+ PrintWriter outPrintWriter = getOutPrintWriter();
+
+ SimulatedBinderProviderEvent simulatedProviderBinderEvent;
+ try {
+ simulatedProviderBinderEvent = SimulatedBinderProviderEvent.createFromArgs(this);
+ } catch (IllegalArgumentException e) {
+ outPrintWriter.println("Error: " + e.getMessage());
+ return 1;
+ }
+
+ outPrintWriter.println("Injecting: " + simulatedProviderBinderEvent);
+ try {
+ mService.simulateBinderProviderEvent(simulatedProviderBinderEvent);
+ } catch (IllegalStateException e) {
+ outPrintWriter.println("Error: " + e.getMessage());
+ return 2;
+ }
+ return 0;
+ }
+
+ @Override
+ public void onHelp() {
+ final PrintWriter pw = getOutPrintWriter();
+ pw.println("Location Time Zone Manager (location_time_zone_manager) commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println(" simulate_binder");
+ pw.println(" <simulated provider binder event>");
+ pw.println();
+ SimulatedBinderProviderEvent.printCommandLineOpts(pw);
+ pw.println();
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java
new file mode 100644
index 0000000..3743779
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java
@@ -0,0 +1,511 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.timezone;
+
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_PERMANENT_FAILURE;
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_SUCCESS;
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_UNCERTAIN;
+
+import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DISABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.location.timezone.LocationTimeZoneEvent;
+import android.os.Handler;
+import android.os.SystemClock;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.timezonedetector.ConfigurationInternal;
+import com.android.server.timezonedetector.Dumpable;
+import com.android.server.timezonedetector.ReferenceWithHistory;
+
+import java.util.Objects;
+
+/**
+ * A facade used by the {@link LocationTimeZoneProviderController} to interact with a location time
+ * zone provider. The provider could have a binder implementation with logic running in another
+ * process, or could be a stubbed instance when no real provider is registered.
+ *
+ * <p>The provider is supplied with a {@link ProviderListener} via {@link
+ * #initialize(ProviderListener)}. This enables it to communicates asynchronous detection / error
+ * events back to the {@link LocationTimeZoneProviderController} via the {@link
+ * ProviderListener#onProviderStateChange} method. This call must be made on the
+ * {@link Handler} thread from the {@link ThreadingDomain} passed to the constructor.
+ *
+ * <p>All incoming calls from the controller except for {@link
+ * LocationTimeZoneProvider#dump(android.util.IndentingPrintWriter, String[])} will be made on the
+ * {@link Handler} thread of the {@link ThreadingDomain} passed to the constructor.
+ */
+abstract class LocationTimeZoneProvider implements Dumpable {
+
+ /**
+ * Listener interface used by the {@link LocationTimeZoneProviderController} to register an
+ * interest in provider events.
+ */
+ interface ProviderListener {
+ /**
+ * Indicated that a provider changed states. The {@code providerState} indicates which one
+ */
+ void onProviderStateChange(@NonNull ProviderState providerState);
+ }
+
+ /**
+ * Information about the provider's current state.
+ */
+ static class ProviderState {
+
+ @IntDef({ PROVIDER_STATE_UNKNOWN, PROVIDER_STATE_ENABLED, PROVIDER_STATE_DISABLED,
+ PROVIDER_STATE_PERM_FAILED })
+ @interface ProviderStateEnum {}
+
+ /**
+ * Uninitialized value. Must not be used afte {@link LocationTimeZoneProvider#initialize}.
+ */
+ static final int PROVIDER_STATE_UNKNOWN = 0;
+
+ /**
+ * The provider is currently enabled.
+ */
+ static final int PROVIDER_STATE_ENABLED = 1;
+
+ /**
+ * The provider is currently disabled.
+ * This is the state after {@link #initialize} is called.
+ */
+ static final int PROVIDER_STATE_DISABLED = 2;
+
+ /**
+ * The provider has failed and cannot be re-enabled.
+ *
+ * Providers may enter this state after a provider is enabled.
+ */
+ static final int PROVIDER_STATE_PERM_FAILED = 3;
+
+ /** The {@link LocationTimeZoneProvider} the state is for. */
+ public final @NonNull LocationTimeZoneProvider provider;
+
+ /** The state enum value of the current state. */
+ public final @ProviderStateEnum int stateEnum;
+
+ /**
+ * The last {@link LocationTimeZoneEvent} received. Only populated when {@link #stateEnum}
+ * is {@link #PROVIDER_STATE_ENABLED}, but it can be {@code null} then too if no event has
+ * yet been received.
+ */
+ @Nullable public final LocationTimeZoneEvent event;
+
+ /**
+ * The user configuration associated with the current state. Only and always present when
+ * {@link #stateEnum} is {@link #PROVIDER_STATE_ENABLED}.
+ */
+ @Nullable public final ConfigurationInternal currentUserConfiguration;
+
+ /**
+ * The time according to the elapsed realtime clock when the provider entered the current
+ * state. Included for debugging, not used for equality.
+ */
+ private final long mStateEntryTimeMillis;
+
+ /**
+ * Debug information providing context for the transition to this state. Included for
+ * debugging, not used for equality.
+ */
+ @Nullable private final String mDebugInfo;
+
+
+ private ProviderState(@NonNull LocationTimeZoneProvider provider,
+ @ProviderStateEnum int stateEnum, @Nullable LocationTimeZoneEvent event,
+ @Nullable ConfigurationInternal currentUserConfiguration,
+ @Nullable String debugInfo) {
+ this.provider = Objects.requireNonNull(provider);
+ this.stateEnum = stateEnum;
+ this.event = event;
+ this.currentUserConfiguration = currentUserConfiguration;
+ this.mStateEntryTimeMillis = SystemClock.elapsedRealtime();
+ this.mDebugInfo = debugInfo;
+ }
+
+ /** Creates the bootstrap state, uses {@link #PROVIDER_STATE_UNKNOWN}. */
+ static ProviderState createStartingState(
+ @NonNull LocationTimeZoneProvider provider) {
+ return new ProviderState(
+ provider, PROVIDER_STATE_UNKNOWN, null, null, "Initial state");
+ }
+
+ /**
+ * Create a new state from this state. Validates that the state transition is valid
+ * and that the required parameters for the new state are present / absent.
+ */
+ ProviderState newState(@ProviderStateEnum int newStateEnum,
+ @Nullable LocationTimeZoneEvent event,
+ @Nullable ConfigurationInternal currentUserConfig,
+ @Nullable String debugInfo) {
+
+ // Check valid "from" transitions.
+ switch (this.stateEnum) {
+ case PROVIDER_STATE_UNKNOWN: {
+ if (newStateEnum != PROVIDER_STATE_DISABLED) {
+ throw new IllegalArgumentException(
+ "Must transition from " + prettyPrintStateEnum(
+ PROVIDER_STATE_UNKNOWN)
+ + " to " + prettyPrintStateEnum(PROVIDER_STATE_DISABLED));
+ }
+ break;
+ }
+ case PROVIDER_STATE_DISABLED:
+ case PROVIDER_STATE_ENABLED: {
+ // These can go to each other or PROVIDER_STATE_PERM_FAILED.
+ break;
+ }
+ case PROVIDER_STATE_PERM_FAILED: {
+ throw new IllegalArgumentException("Illegal transition out of "
+ + prettyPrintStateEnum(PROVIDER_STATE_UNKNOWN));
+ }
+ default: {
+ throw new IllegalArgumentException("Invalid this.stateEnum=" + this.stateEnum);
+ }
+ }
+
+ // Validate "to" transitions / arguments.
+ switch (newStateEnum) {
+ case PROVIDER_STATE_UNKNOWN: {
+ throw new IllegalArgumentException("Cannot transition to "
+ + prettyPrintStateEnum(PROVIDER_STATE_UNKNOWN));
+ }
+ case PROVIDER_STATE_DISABLED: {
+ if (event != null || currentUserConfig != null) {
+ throw new IllegalArgumentException(
+ "Disabled state: event and currentUserConfig must be null"
+ + ", event=" + event
+ + ", currentUserConfig=" + currentUserConfig);
+ }
+ break;
+ }
+ case PROVIDER_STATE_ENABLED: {
+ if (currentUserConfig == null) {
+ throw new IllegalArgumentException(
+ "Enabled state: currentUserConfig must not be null");
+ }
+ break;
+ }
+ case PROVIDER_STATE_PERM_FAILED: {
+ if (event != null || currentUserConfig != null) {
+ throw new IllegalArgumentException(
+ "Perf failed state: event and currentUserConfig must be null"
+ + ", event=" + event
+ + ", currentUserConfig=" + currentUserConfig);
+ }
+ break;
+ }
+ default: {
+ throw new IllegalArgumentException("Unknown newStateEnum=" + newStateEnum);
+ }
+ }
+ return new ProviderState(provider, newStateEnum, event, currentUserConfig, debugInfo);
+ }
+
+ @Override
+ public String toString() {
+ return "State{"
+ + "stateEnum=" + prettyPrintStateEnum(stateEnum)
+ + ", event=" + event
+ + ", currentUserConfiguration=" + currentUserConfiguration
+ + ", mStateEntryTimeMillis=" + mStateEntryTimeMillis
+ + ", mDebugInfo=" + mDebugInfo
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ProviderState state = (ProviderState) o;
+ return stateEnum == state.stateEnum
+ && Objects.equals(event, state.event)
+ && Objects.equals(currentUserConfiguration, state.currentUserConfiguration);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(stateEnum, event, currentUserConfiguration);
+ }
+
+ private static String prettyPrintStateEnum(@ProviderStateEnum int state) {
+ switch (state) {
+ case PROVIDER_STATE_DISABLED:
+ return "Disabled (" + PROVIDER_STATE_DISABLED + ")";
+ case PROVIDER_STATE_ENABLED:
+ return "Enabled (" + PROVIDER_STATE_ENABLED + ")";
+ case PROVIDER_STATE_PERM_FAILED:
+ return "Perm failure (" + PROVIDER_STATE_PERM_FAILED + ")";
+ case PROVIDER_STATE_UNKNOWN:
+ default:
+ return "Unknown (" + state + ")";
+ }
+ }
+ }
+
+ @NonNull final ThreadingDomain mThreadingDomain;
+ @NonNull final Object mSharedLock;
+ @NonNull final String mProviderName;
+
+ /**
+ * The current state (with history for debugging).
+ */
+ @GuardedBy("mSharedLock")
+ final ReferenceWithHistory<ProviderState> mCurrentState =
+ new ReferenceWithHistory<>(10);
+
+ // Non-null and effectively final after initialize() is called.
+ ProviderListener mProviderListener;
+
+ /** Creates the instance. */
+ LocationTimeZoneProvider(@NonNull ThreadingDomain threadingDomain,
+ @NonNull String providerName) {
+ mThreadingDomain = Objects.requireNonNull(threadingDomain);
+ mSharedLock = threadingDomain.getLockObject();
+ mProviderName = Objects.requireNonNull(providerName);
+ }
+
+ /**
+ * Called before the provider is first used.
+ */
+ final void initialize(@NonNull ProviderListener providerListener) {
+ mThreadingDomain.assertCurrentThread();
+
+ synchronized (mSharedLock) {
+ if (mProviderListener != null) {
+ throw new IllegalStateException("initialize already called");
+ }
+ mProviderListener = Objects.requireNonNull(providerListener);
+ ProviderState currentState = ProviderState.createStartingState(this);
+ ProviderState newState = currentState.newState(
+ PROVIDER_STATE_DISABLED, null, null, "initialize() called");
+ setCurrentState(newState, false);
+
+ onInitialize();
+ }
+ }
+
+ /**
+ * Implemented by subclasses to do work during {@link #initialize}.
+ */
+ abstract void onInitialize();
+
+ /**
+ * Set the current state, for use by this class and subclasses only. If {@code #notifyChanges}
+ * is {@code true} and {@code newState} is not equal to the old state, then {@link
+ * ProviderListener#onProviderStateChange(ProviderState)} must be called on
+ * {@link #mProviderListener}.
+ */
+ final void setCurrentState(@NonNull ProviderState newState, boolean notifyChanges) {
+ mThreadingDomain.assertCurrentThread();
+ synchronized (mSharedLock) {
+ ProviderState oldState = mCurrentState.get();
+ mCurrentState.set(newState);
+ onSetCurrentState(newState);
+ if (notifyChanges) {
+ if (!Objects.equals(newState, oldState)) {
+ mProviderListener.onProviderStateChange(newState);
+ }
+ }
+ }
+ }
+
+ /**
+ * Overridden by subclasses to do work during {@link #setCurrentState}.
+ */
+ @GuardedBy("mSharedLock")
+ void onSetCurrentState(ProviderState newState) {
+ // Default no-op.
+ }
+
+ /**
+ * Returns the current state of the provider. This method must be called using the handler
+ * thread from the {@link ThreadingDomain}.
+ */
+ @NonNull
+ final ProviderState getCurrentState() {
+ mThreadingDomain.assertCurrentThread();
+ synchronized (mSharedLock) {
+ return mCurrentState.get();
+ }
+ }
+
+ /**
+ * Returns the name of the provider. This method must be called using the handler thread from
+ * the {@link ThreadingDomain}.
+ */
+ final String getName() {
+ mThreadingDomain.assertCurrentThread();
+ return mProviderName;
+ }
+
+ /**
+ * Enables the provider. It is an error to call this method except when the {@link
+ * #getCurrentState()} is at {@link ProviderState#PROVIDER_STATE_DISABLED}. This method must be
+ * called using the handler thread from the {@link ThreadingDomain}.
+ */
+ final void enable(@NonNull ConfigurationInternal currentUserConfiguration) {
+ mThreadingDomain.assertCurrentThread();
+
+ synchronized (mSharedLock) {
+ assertCurrentState(PROVIDER_STATE_DISABLED);
+
+ ProviderState currentState = getCurrentState();
+ ProviderState newState = currentState.newState(
+ PROVIDER_STATE_ENABLED, null, currentUserConfiguration, "enable() called");
+ setCurrentState(newState, false);
+ onEnable();
+ }
+ }
+
+ /**
+ * Implemented by subclasses to do work during {@link #enable}.
+ */
+ abstract void onEnable();
+
+ /**
+ * Disables the provider. It is an error* to call this method except when the {@link
+ * #getCurrentState()} is at {@link ProviderState#PROVIDER_STATE_ENABLED}. This method must be
+ * called using the handler thread from the {@link ThreadingDomain}.
+ */
+ final void disable() {
+ mThreadingDomain.assertCurrentThread();
+
+ synchronized (mSharedLock) {
+ assertCurrentState(PROVIDER_STATE_ENABLED);
+
+ ProviderState currentState = getCurrentState();
+ ProviderState newState =
+ currentState.newState(PROVIDER_STATE_DISABLED, null, null, "disable() called");
+ setCurrentState(newState, false);
+
+ onDisable();
+ }
+ }
+
+ /**
+ * Implemented by subclasses to do work during {@link #disable}.
+ */
+ abstract void onDisable();
+
+ /** For subclasses to invoke when a {@link LocationTimeZoneEvent} has been received. */
+ final void handleLocationTimeZoneEvent(
+ @NonNull LocationTimeZoneEvent locationTimeZoneEvent) {
+ mThreadingDomain.assertCurrentThread();
+ Objects.requireNonNull(locationTimeZoneEvent);
+
+ synchronized (mSharedLock) {
+ debugLog("handleLocationTimeZoneEvent: mProviderName=" + mProviderName
+ + ", locationTimeZoneEvent=" + locationTimeZoneEvent);
+
+ ProviderState currentState = getCurrentState();
+ int eventType = locationTimeZoneEvent.getEventType();
+ switch (currentState.stateEnum) {
+ case PROVIDER_STATE_PERM_FAILED: {
+ // After entering perm failed, there is nothing to do. The remote peer is
+ // supposed to stop sending events after it has reported perm failure.
+ logWarn("handleLocationTimeZoneEvent: Event=" + locationTimeZoneEvent
+ + " received for provider=" + this + " when in failed state");
+ return;
+ }
+ case PROVIDER_STATE_DISABLED: {
+ switch (eventType) {
+ case EVENT_TYPE_PERMANENT_FAILURE: {
+ String msg = "handleLocationTimeZoneEvent:"
+ + " Failure event=" + locationTimeZoneEvent
+ + " received for disabled provider=" + this
+ + ", entering permanently failed state";
+ logWarn(msg);
+ ProviderState newState = currentState.newState(
+ PROVIDER_STATE_PERM_FAILED, null, null, msg);
+ setCurrentState(newState, true);
+ return;
+ }
+ case EVENT_TYPE_SUCCESS:
+ case EVENT_TYPE_UNCERTAIN: {
+ // Any geolocation-related events received for a disabled provider are
+ // ignored: they should not happen.
+ logWarn("handleLocationTimeZoneEvent:"
+ + " event=" + locationTimeZoneEvent
+ + " received for disabled provider=" + this
+ + ", ignoring");
+
+ return;
+ }
+ default: {
+ throw new IllegalStateException(
+ "Unknown eventType=" + locationTimeZoneEvent);
+ }
+ }
+ }
+ case PROVIDER_STATE_ENABLED: {
+ switch (eventType) {
+ case EVENT_TYPE_PERMANENT_FAILURE: {
+ String msg = "handleLocationTimeZoneEvent:"
+ + " Failure event=" + locationTimeZoneEvent
+ + " received for provider=" + this
+ + ", entering permanently failed state";
+ logWarn(msg);
+ ProviderState newState = currentState.newState(
+ PROVIDER_STATE_PERM_FAILED, null, null, msg);
+ setCurrentState(newState, true);
+ return;
+ }
+ case EVENT_TYPE_UNCERTAIN:
+ case EVENT_TYPE_SUCCESS: {
+ ProviderState newState = currentState.newState(PROVIDER_STATE_ENABLED,
+ locationTimeZoneEvent, currentState.currentUserConfiguration,
+ "handleLocationTimeZoneEvent() when enabled");
+ setCurrentState(newState, true);
+ return;
+ }
+ default: {
+ throw new IllegalStateException(
+ "Unknown eventType=" + locationTimeZoneEvent);
+ }
+ }
+ }
+ default: {
+ throw new IllegalStateException("Unknown providerType=" + currentState);
+ }
+ }
+ }
+ }
+
+ /**
+ * Implemented by subclasses.
+ */
+ abstract void logWarn(String msg);
+
+ private void assertCurrentState(@ProviderState.ProviderStateEnum int requiredState) {
+ ProviderState currentState = getCurrentState();
+ if (currentState.stateEnum != requiredState) {
+ throw new IllegalStateException(
+ "Required stateEnum=" + requiredState + ", but was " + currentState);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java
new file mode 100644
index 0000000..2f75c43
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.timezone;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+
+import com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState;
+import com.android.server.timezonedetector.ConfigurationInternal;
+import com.android.server.timezonedetector.Dumpable;
+import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion;
+
+import java.util.Objects;
+
+/**
+ * An base class for the component responsible handling events from {@link
+ * LocationTimeZoneProvider}s and synthesizing time zone ID suggestions for sending to the time zone
+ * detector. This interface primarily exists to extract testable detection logic, i.e. with
+ * a minimal number of threading considerations or dependencies on Android infrastructure.
+ *
+ * <p>The controller interacts with the following components:
+ * <ul>
+ * <li>The surrounding service, which calls {@link #initialize(Environment, Callback)} and
+ * {@link #onConfigChanged()}.</li>
+ * <li>The {@link Environment} through which obtains information it needs.</li>
+ * <li>The {@link Callback} through which it makes time zone suggestions.</li>
+ * <li>Any {@link LocationTimeZoneProvider} instances it owns, which communicate via the
+ * {@link LocationTimeZoneProvider.ProviderListener#onProviderStateChange(ProviderState)}
+ * method.</li>
+ * </ul>
+ *
+ * <p>All incoming calls except for {@link
+ * LocationTimeZoneProviderController#dump(android.util.IndentingPrintWriter, String[])} must be
+ * made on the {@link Handler} thread of the {@link ThreadingDomain} passed to {@link
+ * #LocationTimeZoneProviderController(ThreadingDomain)}.
+ *
+ * <p>Provider / controller integration notes:
+ *
+ * <p>Providers distinguish between "unknown unknowns" ("uncertain") and "known unknowns"
+ * ("certain"), i.e. a provider can be uncertain and not know what the time zone is, which is
+ * different from the certainty that there are no time zone IDs for the current location. A provider
+ * can be certain about there being no time zone IDs for a location for good reason, e.g. for
+ * disputed areas and oceans. Distinguishing uncertainty allows the controller to try other
+ * providers (or give up), where as certainty means it should not.
+ *
+ * <p>A provider can fail permanently. A permanent failure will disable the provider until next
+ * boot.
+ */
+abstract class LocationTimeZoneProviderController implements Dumpable {
+
+ @NonNull protected final ThreadingDomain mThreadingDomain;
+ @NonNull protected final Object mSharedLock;
+
+ LocationTimeZoneProviderController(@NonNull ThreadingDomain threadingDomain) {
+ mThreadingDomain = Objects.requireNonNull(threadingDomain);
+ mSharedLock = threadingDomain.getLockObject();
+ }
+
+ /**
+ * Called to initialize the controller during boot. Called once only.
+ * {@link LocationTimeZoneProvider#initialize} must be called by this method.
+ */
+ abstract void initialize(@NonNull Environment environment, @NonNull Callback callback);
+
+ /**
+ * Called when any settings or other device state that affect location-based time zone detection
+ * have changed. The receiver should call {@link
+ * Environment#getCurrentUserConfigurationInternal()} to get the current user's config. This
+ * call must be made on the {@link ThreadingDomain} handler thread.
+ */
+ abstract void onConfigChanged();
+
+ /**
+ * Used by {@link LocationTimeZoneProviderController} to obtain information from the surrounding
+ * service. It can easily be faked for tests.
+ */
+ abstract static class Environment {
+
+ @NonNull protected final ThreadingDomain mThreadingDomain;
+ @NonNull protected final Object mSharedLock;
+
+ Environment(@NonNull ThreadingDomain threadingDomain) {
+ mThreadingDomain = Objects.requireNonNull(threadingDomain);
+ mSharedLock = threadingDomain.getLockObject();
+ }
+
+ /** Returns the {@link ConfigurationInternal} for the current user of the device. */
+ abstract ConfigurationInternal getCurrentUserConfigurationInternal();
+ }
+
+ /**
+ * Used by {@link LocationTimeZoneProviderController} to interact with the surrounding service.
+ * It can easily be faked for tests.
+ */
+ abstract static class Callback {
+
+ @NonNull protected final ThreadingDomain mThreadingDomain;
+ @NonNull protected final Object mSharedLock;
+
+ Callback(@NonNull ThreadingDomain threadingDomain) {
+ mThreadingDomain = Objects.requireNonNull(threadingDomain);
+ mSharedLock = threadingDomain.getLockObject();
+ }
+
+ /**
+ * Suggests the latest time zone state for the device.
+ */
+ abstract void suggest(@NonNull GeolocationTimeZoneSuggestion suggestion);
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderProxy.java
new file mode 100644
index 0000000..3d889ae
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderProxy.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.timezone;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.location.timezone.LocationTimeZoneEvent;
+import android.os.Handler;
+import android.util.IndentingPrintWriter;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.location.timezone.LocationTimeZoneProviderRequest;
+import com.android.server.timezonedetector.Dumpable;
+
+import java.util.Objects;
+
+/**
+ * System server-side proxy for ILocationTimeZoneProvider implementations, i.e. this provides the
+ * system server object used to communicate with a remote LocationTimeZoneProvider over Binder,
+ * which could be running in a different process. As LocationTimeZoneProviders are bound / unbound
+ * this proxy will rebind to the "best" available remote process.
+ *
+ * <p>Threading guarantees provided / required by this interface:
+ * <ul>
+ * <li>All public methods defined by this class must be invoked using the {@link Handler} thread
+ * from the {@link ThreadingDomain} passed to the constructor, excluding
+ * {@link #dump(IndentingPrintWriter, String[])}</li>
+ * <li>Non-static public methods that make binder calls to remote processes (e.g.
+ * {@link #setRequest(LocationTimeZoneProviderRequest)}) are executed asynchronously and will
+ * return immediately.</li>
+ * <li>Callbacks received via binder are delivered via {@link Listener} are delivered on the
+ * {@link Handler} thread from the {@link ThreadingDomain} passed to the constructor.
+ * </ul>
+ *
+ * <p>This class exists to enable the introduction of test implementations of {@link
+ * LocationTimeZoneProviderProxy} that can be used when a device is in a test mode to inject test
+ * events / behavior that are otherwise difficult to simulate.
+ */
+abstract class LocationTimeZoneProviderProxy implements Dumpable {
+
+ @NonNull protected final Context mContext;
+ @NonNull protected final ThreadingDomain mThreadingDomain;
+ @NonNull protected final Object mSharedLock;
+
+ // Non-null and effectively final after setListener() is called.
+ @GuardedBy("mSharedLock")
+ @Nullable
+ protected Listener mListener;
+
+ LocationTimeZoneProviderProxy(
+ @NonNull Context context, @NonNull ThreadingDomain threadingDomain) {
+ mContext = Objects.requireNonNull(context);
+ mThreadingDomain = Objects.requireNonNull(threadingDomain);
+ mSharedLock = threadingDomain.getLockObject();
+ }
+
+ /**
+ * Sets the listener. The listener can expect to receive all events after this point.
+ */
+ void setListener(@NonNull Listener listener) {
+ Objects.requireNonNull(listener);
+ synchronized (mSharedLock) {
+ if (mListener != null) {
+ throw new IllegalStateException("listener already set");
+ }
+ this.mListener = listener;
+ }
+ }
+
+ /**
+ * Sets a new request for the provider.
+ */
+ abstract void setRequest(@NonNull LocationTimeZoneProviderRequest request);
+
+ /**
+ * Handles a {@link LocationTimeZoneEvent} from a remote process.
+ */
+ final void handleLocationTimeZoneEvent(
+ @NonNull LocationTimeZoneEvent locationTimeZoneEvent) {
+ // These calls are invoked on a binder thread. Move to the mThreadingDomain thread as
+ // required by the guarantees for this class.
+ mThreadingDomain.post(() -> mListener.onReportLocationTimeZoneEvent(locationTimeZoneEvent));
+ }
+
+ /**
+ * Interface for listening to location time zone providers. See {@link
+ * LocationTimeZoneProviderProxy} for threading guarantees.
+ */
+ interface Listener {
+
+ /**
+ * Called when a provider receives a {@link LocationTimeZoneEvent}.
+ */
+ void onReportLocationTimeZoneEvent(@NonNull LocationTimeZoneEvent locationTimeZoneEvent);
+
+ /**
+ * Called when a provider is (re)bound.
+ */
+ void onProviderBound();
+
+ /** Called when a provider is unbound. */
+ void onProviderUnbound();
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProvider.java b/services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProvider.java
new file mode 100644
index 0000000..79e2b97
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProvider.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.timezone;
+
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.IndentingPrintWriter;
+import android.util.Slog;
+
+/**
+ * A {@link LocationTimeZoneProvider} that provides minimal responses needed for the {@link
+ * LocationTimeZoneProviderController} to operate correctly when there is no "real" provider
+ * configured. This can be used during development / testing, or in a production build when the
+ * platform supports more providers than are needed for an Android deployment.
+ *
+ * <p>For example, if the {@link LocationTimeZoneProviderController} supports a primary
+ * and a secondary {@link LocationTimeZoneProvider}, but only a primary is configured, the secondary
+ * config will be left null and the {@link LocationTimeZoneProvider} implementation will be
+ * defaulted to a {@link NullLocationTimeZoneProvider}. The {@link NullLocationTimeZoneProvider}
+ * enters a {@link ProviderState#PROVIDER_STATE_PERM_FAILED} state immediately after being enabled
+ * for the first time and sends the appropriate event, which ensures the {@link
+ * LocationTimeZoneProviderController} won't expect any further {@link
+ * android.location.timezone.LocationTimeZoneEvent}s to come from it, and won't attempt to use it
+ * again.
+ */
+class NullLocationTimeZoneProvider extends LocationTimeZoneProvider {
+
+ private static final String TAG = "NullLocationTimeZoneProvider";
+
+ /** Creates the instance. */
+ NullLocationTimeZoneProvider(@NonNull ThreadingDomain threadingDomain,
+ @NonNull String providerName) {
+ super(threadingDomain, providerName);
+ }
+
+ @Override
+ void onInitialize() {
+ // No-op
+ }
+
+ @Override
+ void onEnable() {
+ // Report a failure (asynchronously using the mThreadingDomain thread to avoid recursion).
+ mThreadingDomain.post(()-> {
+ // Enter the perm-failed state.
+ ProviderState currentState = mCurrentState.get();
+ ProviderState failedState = currentState.newState(
+ PROVIDER_STATE_PERM_FAILED, null, null, "Stubbed provider");
+ setCurrentState(failedState, true);
+ });
+ }
+
+ @Override
+ void onDisable() {
+ // Ignored - NullLocationTimeZoneProvider is always permanently failed.
+ }
+
+ @Override
+ void logWarn(String msg) {
+ Slog.w(TAG, msg);
+ }
+
+ @Override
+ public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
+ synchronized (mSharedLock) {
+ ipw.println("{Stubbed LocationTimeZoneProvider}");
+ ipw.println("mProviderName=" + mProviderName);
+ ipw.println("mCurrentState=" + mCurrentState);
+ }
+ }
+
+ @Override
+ public String toString() {
+ synchronized (mSharedLock) {
+ return "NullLocationTimeZoneProvider{"
+ + "mProviderName='" + mProviderName + '\''
+ + "mCurrentState='" + mCurrentState + '\''
+ + '}';
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/SimulatedBinderProviderEvent.java b/services/core/java/com/android/server/location/timezone/SimulatedBinderProviderEvent.java
new file mode 100644
index 0000000..ef2e349
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/SimulatedBinderProviderEvent.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.timezone;
+
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_PERMANENT_FAILURE;
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_SUCCESS;
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_UNCERTAIN;
+
+import static com.android.server.location.timezone.LocationTimeZoneManagerService.PRIMARY_PROVIDER_NAME;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.location.timezone.LocationTimeZoneEvent;
+import android.os.ShellCommand;
+import android.os.SystemClock;
+import android.os.UserHandle;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * An event used for simulating real binder proxy behavior using a {@link
+ * SimulatedLocationTimeZoneProviderProxy}.
+ */
+final class SimulatedBinderProviderEvent {
+
+ private static final List<String> VALID_PROVIDER_NAMES = Arrays.asList(PRIMARY_PROVIDER_NAME);
+
+ static final int INJECTED_EVENT_TYPE_ON_BIND = 1;
+ static final int INJECTED_EVENT_TYPE_ON_UNBIND = 2;
+ static final int INJECTED_EVENT_TYPE_LOCATION_TIME_ZONE_EVENT = 3;
+
+
+ @NonNull private final String mProviderName;
+ private final int mEventType;
+ @Nullable private final LocationTimeZoneEvent mLocationTimeZoneEvent;
+
+ private SimulatedBinderProviderEvent(@NonNull String providerName, int eventType,
+ @Nullable LocationTimeZoneEvent locationTimeZoneEvent) {
+ this.mProviderName = Objects.requireNonNull(providerName);
+ this.mEventType = eventType;
+ this.mLocationTimeZoneEvent = locationTimeZoneEvent;
+ }
+
+ @NonNull
+ String getProviderName() {
+ return mProviderName;
+ }
+
+ @Nullable
+ LocationTimeZoneEvent getLocationTimeZoneEvent() {
+ return mLocationTimeZoneEvent;
+ }
+
+ int getEventType() {
+ return mEventType;
+ }
+
+ /** Prints the command line options that {@link #createFromArgs(ShellCommand)} understands. */
+ static void printCommandLineOpts(PrintWriter pw) {
+ pw.println("Simulated provider binder event:");
+ pw.println();
+ pw.println("<provider name> [onBind|onUnbind|locationTimeZoneEvent"
+ + " <location time zone event args>]");
+ pw.println();
+ pw.println("<provider name> = " + VALID_PROVIDER_NAMES);
+ pw.println("<location time zone event args> ="
+ + " [PERMANENT_FAILURE|UNCERTAIN|SUCCESS <time zone ids>*]");
+ }
+
+ /**
+ * Constructs a {@link SimulatedBinderProviderEvent} from the arguments of {@code shellCommand}.
+ */
+ static SimulatedBinderProviderEvent createFromArgs(ShellCommand shellCommand) {
+ String providerName = shellCommand.getNextArgRequired();
+ if (!VALID_PROVIDER_NAMES.contains(providerName)) {
+ throw new IllegalArgumentException("Unknown provider name=" + providerName);
+ }
+ String injectedEvent = shellCommand.getNextArgRequired();
+ switch (injectedEvent) {
+ case "onBind": {
+ return new SimulatedBinderProviderEvent(
+ providerName, INJECTED_EVENT_TYPE_ON_BIND, null);
+ }
+ case "onUnbind": {
+ return new SimulatedBinderProviderEvent(
+ providerName, INJECTED_EVENT_TYPE_ON_UNBIND, null);
+ }
+ case "locationTimeZoneEvent": {
+ LocationTimeZoneEvent event = parseLocationTimeZoneEventArgs(shellCommand);
+ return new SimulatedBinderProviderEvent(providerName,
+ INJECTED_EVENT_TYPE_LOCATION_TIME_ZONE_EVENT, event);
+ }
+ default: {
+ throw new IllegalArgumentException("Unknown simulated event type=" + injectedEvent);
+ }
+ }
+ }
+
+ private static LocationTimeZoneEvent parseLocationTimeZoneEventArgs(ShellCommand shellCommand) {
+ LocationTimeZoneEvent.Builder eventBuilder = new LocationTimeZoneEvent.Builder()
+ .setElapsedRealtimeNanos(SystemClock.elapsedRealtime())
+ .setUserHandle(UserHandle.of(ActivityManager.getCurrentUser()));
+
+ String eventTypeString = shellCommand.getNextArgRequired();
+ switch (eventTypeString.toUpperCase()) {
+ case "PERMANENT_FAILURE": {
+ eventBuilder.setEventType(EVENT_TYPE_PERMANENT_FAILURE);
+ break;
+ }
+ case "UNCERTAIN": {
+ eventBuilder.setEventType(EVENT_TYPE_UNCERTAIN);
+ break;
+ }
+ case "SUCCESS": {
+ eventBuilder.setEventType(EVENT_TYPE_SUCCESS)
+ .setTimeZoneIds(parseTimeZoneArgs(shellCommand));
+ break;
+ }
+ default: {
+ throw new IllegalArgumentException("Error: Unknown eventType: " + eventTypeString);
+ }
+ }
+ return eventBuilder.build();
+ }
+
+ private static List<String> parseTimeZoneArgs(ShellCommand shellCommand) {
+ List<String> timeZoneIds = new ArrayList<>();
+ String timeZoneId;
+ while ((timeZoneId = shellCommand.getNextArg()) != null) {
+ timeZoneIds.add(timeZoneId);
+ }
+ return timeZoneIds;
+ }
+
+ @Override
+ public String toString() {
+ return "SimulatedBinderProviderEvent{"
+ + "mProviderName=" + mProviderName
+ + ", mEventType=" + mEventType
+ + ", mLocationTimeZoneEvent=" + mLocationTimeZoneEvent
+ + '}';
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/SimulatedLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/location/timezone/SimulatedLocationTimeZoneProviderProxy.java
new file mode 100644
index 0000000..462bcab
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/SimulatedLocationTimeZoneProviderProxy.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.timezone;
+
+import static com.android.server.location.timezone.SimulatedBinderProviderEvent.INJECTED_EVENT_TYPE_LOCATION_TIME_ZONE_EVENT;
+import static com.android.server.location.timezone.SimulatedBinderProviderEvent.INJECTED_EVENT_TYPE_ON_BIND;
+import static com.android.server.location.timezone.SimulatedBinderProviderEvent.INJECTED_EVENT_TYPE_ON_UNBIND;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.IndentingPrintWriter;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.location.timezone.LocationTimeZoneProviderRequest;
+import com.android.server.timezonedetector.ReferenceWithHistory;
+
+import java.util.Objects;
+
+/**
+ * A replacement for a real binder proxy for use during integration testing
+ * that can be used to inject simulated {@link LocationTimeZoneProviderProxy} behavior.
+ */
+class SimulatedLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy {
+
+ @GuardedBy("mProxyLock")
+ @NonNull private LocationTimeZoneProviderRequest mRequest;
+
+ @NonNull private ReferenceWithHistory<String> mLastEvent = new ReferenceWithHistory<>(50);
+
+ SimulatedLocationTimeZoneProviderProxy(
+ @NonNull Context context, @NonNull ThreadingDomain threadingDomain) {
+ super(context, threadingDomain);
+ mRequest = LocationTimeZoneProviderRequest.EMPTY_REQUEST;
+ }
+
+ void simulate(@NonNull SimulatedBinderProviderEvent event) {
+ switch (event.getEventType()) {
+ case INJECTED_EVENT_TYPE_ON_BIND: {
+ mLastEvent.set("Simulating onProviderBound(), event=" + event);
+ mThreadingDomain.post(this::onBindOnHandlerThread);
+ break;
+ }
+ case INJECTED_EVENT_TYPE_ON_UNBIND: {
+ mLastEvent.set("Simulating onProviderUnbound(), event=" + event);
+ mThreadingDomain.post(this::onUnbindOnHandlerThread);
+ break;
+ }
+ case INJECTED_EVENT_TYPE_LOCATION_TIME_ZONE_EVENT: {
+ if (!mRequest.getReportLocationTimeZone()) {
+ mLastEvent.set("Test event=" + event + " is testing an invalid case:"
+ + " reporting is off. mRequest=" + mRequest);
+ }
+ mLastEvent.set("Simulating LocationTimeZoneEvent, event=" + event);
+ handleLocationTimeZoneEvent(event.getLocationTimeZoneEvent());
+ break;
+ }
+ default: {
+ mLastEvent.set("Unknown simulated event type. event=" + event);
+ throw new IllegalArgumentException("Unknown simulated event type. event=" + event);
+ }
+ }
+ }
+
+ private void onBindOnHandlerThread() {
+ mThreadingDomain.assertCurrentThread();
+
+ synchronized (mSharedLock) {
+ mListener.onProviderBound();
+ }
+ }
+
+ private void onUnbindOnHandlerThread() {
+ mThreadingDomain.assertCurrentThread();
+
+ synchronized (mSharedLock) {
+ mListener.onProviderUnbound();
+ }
+ }
+
+ @Override
+ final void setRequest(@NonNull LocationTimeZoneProviderRequest request) {
+ mThreadingDomain.assertCurrentThread();
+
+ Objects.requireNonNull(request);
+ synchronized (mSharedLock) {
+ mLastEvent.set("Request received: " + request);
+ mRequest = request;
+ }
+ }
+
+ @Override
+ public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
+ synchronized (mSharedLock) {
+ ipw.println("mRequest=" + mRequest);
+ ipw.println("mLastEvent=" + mLastEvent);
+
+ ipw.increaseIndent();
+ ipw.println("Last event history:");
+ mLastEvent.dump(ipw);
+ ipw.decreaseIndent();
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/ThreadingDomain.java b/services/core/java/com/android/server/location/timezone/ThreadingDomain.java
new file mode 100644
index 0000000..9b9c823
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/ThreadingDomain.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.timezone;
+
+import android.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * A class that can be used to enforce / indicate a set of components that need to share threading
+ * behavior such as a shared lock object and a common thread, with async execution support.
+ *
+ * <p>It is <em>not</em> essential that the object returned by {@link #getLockObject()} is only used
+ * when executing on the domain's thread, but users should be careful to avoid deadlocks when
+ * multiple locks / threads are in use. Generally sticking to a single thread / lock is safest.
+ */
+abstract class ThreadingDomain {
+
+ @NonNull private final Object mLockObject;
+
+ ThreadingDomain() {
+ mLockObject = new Object();
+ }
+
+ /**
+ * Returns the common lock object for this threading domain that can be used for synchronized ()
+ * blocks. The lock is unique to this threading domain.
+ */
+ @NonNull
+ Object getLockObject() {
+ return mLockObject;
+ }
+
+ /**
+ * Returns the Thread associated with this threading domain.
+ */
+ @NonNull
+ abstract Thread getThread();
+
+ /**
+ * Asserts the currently executing thread is the one associated with this threading domain.
+ * Generally useful for documenting expectations in the code. By asserting a single thread is
+ * being used within a set of components, a lot of races can be avoided.
+ */
+ void assertCurrentThread() {
+ Preconditions.checkArgument(Thread.currentThread() == getThread());
+ }
+
+ /**
+ * Execute the supplied runnable on the threading domain's thread.
+ */
+ abstract void post(@NonNull Runnable runnable);
+
+ /**
+ * Execute the supplied runnable on the threading domain's thread with a delay.
+ */
+ abstract void postDelayed(@NonNull Runnable runnable, long delayMillis);
+
+ abstract void postDelayed(Runnable r, Object token, long delayMillis);
+
+ abstract void removeQueuedRunnables(Object token);
+
+ /**
+ * Creates a new {@link SingleRunnableQueue} that can be used to ensure that (at most) a
+ * single runnable for a given purpose is ever queued. Create new ones for different purposes.
+ */
+ SingleRunnableQueue createSingleRunnableQueue() {
+ return new SingleRunnableQueue();
+ }
+
+ /**
+ * A class that allows up to one {@link Runnable} to be queued on the handler, i.e. calling any
+ * of the methods will cancel the execution of any previously queued / delayed runnable. All
+ * methods must be called from the {@link ThreadingDomain}'s thread.
+ */
+ final class SingleRunnableQueue {
+
+ /**
+ * Runs the supplied {@link Runnable} synchronously on the threading domain's thread,
+ * cancelling any queued but not-yet-executed {@link Runnable} previously added by this.
+ * This method must be called from the threading domain's thread.
+ */
+ void runSynchronously(Runnable r) {
+ cancel();
+ r.run();
+ }
+
+ /**
+ * Posts the supplied {@link Runnable} asynchronously and delayed on the threading domain
+ * handler thread, cancelling any queued but not-yet-executed {@link Runnable} previously
+ * added by this. This method must be called from the threading domain's thread.
+ */
+ void runDelayed(Runnable r, long delayMillis) {
+ cancel();
+ ThreadingDomain.this.postDelayed(r, this, delayMillis);
+ }
+
+ /**
+ * Cancels any queued but not-yet-executed {@link Runnable} previously added by this.
+ */
+ public void cancel() {
+ assertCurrentThread();
+ removeQueuedRunnables(this);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
index 7501d9f..73322a6 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
@@ -57,8 +57,12 @@
private static final String TAG = "TimeZoneDetectorService";
- /** A compile time switch for enabling / disabling geolocation-based time zone detection. */
- private static final boolean GEOLOCATION_TIME_ZONE_DETECTION_ENABLED = false;
+ /**
+ * A compile time constant "feature switch" for enabling / disabling location-based time zone
+ * detection on Android. If this is {@code false}, there should be few / little changes in
+ * behavior with previous releases and little overhead associated with geolocation components.
+ */
+ public static final boolean GEOLOCATION_TIME_ZONE_DETECTION_ENABLED = false;
/**
* Handles the service lifecycle for {@link TimeZoneDetectorService} and
diff --git a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
index 95d4ba7..678308a 100644
--- a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
+++ b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
@@ -36,6 +36,9 @@
using android::base::StringPrintf;
using android::base::WriteStringToFile;
+#define SYNC_RECEIVED_WHILE_FROZEN (1)
+#define ASYNC_RECEIVED_WHILE_FROZEN (2)
+
namespace android {
// This performs per-process reclaim on all processes belonging to non-app UIDs.
@@ -99,12 +102,37 @@
}
}
+static jint com_android_server_am_CachedAppOptimizer_getBinderFreezeInfo(JNIEnv *env,
+ jobject clazz, jint pid) {
+ bool syncReceived = false, asyncReceived = false;
+
+ int error = IPCThreadState::getProcessFreezeInfo(pid, &syncReceived, &asyncReceived);
+
+ if (error < 0) {
+ jniThrowException(env, "java/lang/RuntimeException", strerror(error));
+ }
+
+ jint retVal = 0;
+
+ if(syncReceived) {
+ retVal |= SYNC_RECEIVED_WHILE_FROZEN;;
+ }
+
+ if(asyncReceived) {
+ retVal |= ASYNC_RECEIVED_WHILE_FROZEN;
+ }
+
+ return retVal;
+}
+
static const JNINativeMethod sMethods[] = {
/* name, signature, funcPtr */
{"compactSystem", "()V", (void*)com_android_server_am_CachedAppOptimizer_compactSystem},
{"enableFreezerInternal", "(Z)V",
(void*)com_android_server_am_CachedAppOptimizer_enableFreezerInternal},
- {"freezeBinder", "(IZ)V", (void*)com_android_server_am_CachedAppOptimizer_freezeBinder}
+ {"freezeBinder", "(IZ)V", (void*)com_android_server_am_CachedAppOptimizer_freezeBinder},
+ {"getBinderFreezeInfo", "(I)I",
+ (void*)com_android_server_am_CachedAppOptimizer_getBinderFreezeInfo}
};
int register_android_server_am_CachedAppOptimizer(JNIEnv* env)
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index ddd2377..eca9f15 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -293,6 +293,8 @@
"com.android.server.timedetector.TimeDetectorService$Lifecycle";
private static final String TIME_ZONE_DETECTOR_SERVICE_CLASS =
"com.android.server.timezonedetector.TimeZoneDetectorService$Lifecycle";
+ private static final String LOCATION_TIME_ZONE_MANAGER_SERVICE_CLASS =
+ "com.android.server.location.timezone.LocationTimeZoneManagerService$Lifecycle";
private static final String ACCESSIBILITY_MANAGER_SERVICE_CLASS =
"com.android.server.accessibility.AccessibilityManagerService$Lifecycle";
private static final String ADB_SERVICE_CLASS =
@@ -1621,7 +1623,7 @@
try {
mSystemServiceManager.startService(TIME_DETECTOR_SERVICE_CLASS);
} catch (Throwable e) {
- reportWtf("starting StartTimeDetectorService service", e);
+ reportWtf("starting TimeDetectorService service", e);
}
t.traceEnd();
@@ -1629,7 +1631,15 @@
try {
mSystemServiceManager.startService(TIME_ZONE_DETECTOR_SERVICE_CLASS);
} catch (Throwable e) {
- reportWtf("starting StartTimeZoneDetectorService service", e);
+ reportWtf("starting TimeZoneDetectorService service", e);
+ }
+ t.traceEnd();
+
+ t.traceBegin("StartLocationTimeZoneManagerService");
+ try {
+ mSystemServiceManager.startService(LOCATION_TIME_ZONE_MANAGER_SERVICE_CLASS);
+ } catch (Throwable e) {
+ reportWtf("starting LocationTimeZoneManagerService service", e);
}
t.traceEnd();
diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/ControllerImplTest.java b/services/tests/servicestests/src/com/android/server/location/timezone/ControllerImplTest.java
new file mode 100644
index 0000000..dbaad66
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/location/timezone/ControllerImplTest.java
@@ -0,0 +1,528 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.location.timezone;
+
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_SUCCESS;
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_UNCERTAIN;
+
+import static com.android.server.location.timezone.ControllerImpl.UNCERTAINTY_DELAY;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DISABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED;
+import static com.android.server.location.timezone.TestSupport.USER1_CONFIG_GEO_DETECTION_DISABLED;
+import static com.android.server.location.timezone.TestSupport.USER1_CONFIG_GEO_DETECTION_ENABLED;
+import static com.android.server.location.timezone.TestSupport.USER1_ID;
+import static com.android.server.location.timezone.TestSupport.USER2_CONFIG_GEO_DETECTION_ENABLED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import static java.util.Arrays.asList;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.location.timezone.LocationTimeZoneEvent;
+import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
+import android.util.IndentingPrintWriter;
+
+import com.android.server.timezonedetector.ConfigurationInternal;
+import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion;
+import com.android.server.timezonedetector.TestState;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/** Tests for {@link ControllerImpl}. */
+@Presubmit
+public class ControllerImplTest {
+
+ private static final long ARBITRARY_TIME = 12345L;
+
+ private static final LocationTimeZoneEvent USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1 =
+ createLocationTimeZoneEvent(USER1_ID, EVENT_TYPE_SUCCESS, asList("Europe/London"));
+ private static final LocationTimeZoneEvent USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2 =
+ createLocationTimeZoneEvent(USER1_ID, EVENT_TYPE_SUCCESS, asList("Europe/Paris"));
+ private static final LocationTimeZoneEvent USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT =
+ createLocationTimeZoneEvent(USER1_ID, EVENT_TYPE_UNCERTAIN, null);
+
+ private TestThreadingDomain mTestThreadingDomain;
+ private TestCallback mTestCallback;
+ private TestLocationTimeZoneProvider mTestLocationTimeZoneProvider;
+
+ @Before
+ public void setUp() {
+ // For simplicity, the TestThreadingDomain uses the test's main thread. To execute posted
+ // runnables, the test must call methods on mTestThreadingDomain otherwise those runnables
+ // will never get a chance to execute.
+ mTestThreadingDomain = new TestThreadingDomain();
+ mTestCallback = new TestCallback(mTestThreadingDomain);
+ mTestLocationTimeZoneProvider =
+ new TestLocationTimeZoneProvider(mTestThreadingDomain, "primary");
+ }
+
+ @Test
+ public void initialState_enabled() {
+ ControllerImpl controllerImpl =
+ new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
+ TestEnvironment testEnvironment = new TestEnvironment(
+ mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED);
+ controllerImpl.initialize(testEnvironment, mTestCallback);
+
+ mTestLocationTimeZoneProvider.assertInitialized();
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertNextQueueItemIsDelayed(UNCERTAINTY_DELAY);
+ mTestCallback.assertNoSuggestionMade();
+ }
+
+ @Test
+ public void initialState_disabled() {
+ ControllerImpl controllerImpl =
+ new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
+ TestEnvironment testEnvironment = new TestEnvironment(
+ mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_DISABLED);
+ controllerImpl.initialize(testEnvironment, mTestCallback);
+
+ mTestLocationTimeZoneProvider.assertInitialized();
+
+ mTestLocationTimeZoneProvider.assertIsDisabled();
+ mTestThreadingDomain.assertQueueEmpty();
+ mTestCallback.assertNoSuggestionMade();
+ }
+
+ @Test
+ public void enabled_uncertaintySuggestionSentIfNoEventReceived() {
+ ControllerImpl controllerImpl =
+ new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
+ TestEnvironment testEnvironment = new TestEnvironment(
+ mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED);
+ controllerImpl.initialize(testEnvironment, mTestCallback);
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestCallback.assertNoSuggestionMade();
+ mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
+
+ // Simulate time passing with no event being received.
+ mTestThreadingDomain.executeNext();
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestCallback.assertUncertainSuggestionMadeAndCommit();
+ mTestThreadingDomain.assertQueueEmpty();
+ }
+
+ @Test
+ public void enabled_uncertaintySuggestionCancelledIfEventReceived() {
+ ControllerImpl controllerImpl =
+ new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
+ TestEnvironment testEnvironment = new TestEnvironment(
+ mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED);
+ controllerImpl.initialize(testEnvironment, mTestCallback);
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
+ mTestCallback.assertNoSuggestionMade();
+
+ // Simulate a location event being received by the provider. This should cause a suggestion
+ // to be made, and the timeout to be cleared.
+ mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertQueueEmpty();
+ mTestCallback.assertSuggestionMadeAndCommit(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds());
+ }
+
+ @Test
+ public void enabled_repeatedCertainty() {
+ ControllerImpl controllerImpl =
+ new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
+ TestEnvironment testEnvironment = new TestEnvironment(
+ mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED);
+ controllerImpl.initialize(testEnvironment, mTestCallback);
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
+ mTestCallback.assertNoSuggestionMade();
+
+ // Simulate a location event being received by the provider. This should cause a suggestion
+ // to be made, and the timeout to be cleared.
+ mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertQueueEmpty();
+ mTestCallback.assertSuggestionMadeAndCommit(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds());
+
+ // A second, identical event should not cause another suggestion.
+ mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertQueueEmpty();
+ mTestCallback.assertNoSuggestionMade();
+
+ // And a third, different event should cause another suggestion.
+ mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2);
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertQueueEmpty();
+ mTestCallback.assertSuggestionMadeAndCommit(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getTimeZoneIds());
+ }
+
+ @Test
+ public void enabled_briefUncertaintyTriggersNoSuggestion() {
+ ControllerImpl controllerImpl =
+ new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
+ TestEnvironment testEnvironment = new TestEnvironment(
+ mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED);
+ controllerImpl.initialize(testEnvironment, mTestCallback);
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
+ mTestCallback.assertNoSuggestionMade();
+
+ // Simulate a location event being received by the provider. This should cause a suggestion
+ // to be made, and the timeout to be cleared.
+ mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertQueueEmpty();
+ mTestCallback.assertSuggestionMadeAndCommit(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds());
+
+ // Uncertainty should cause
+ mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
+ USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT);
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
+ mTestCallback.assertNoSuggestionMade();
+
+ // And a third event should cause yet another suggestion.
+ mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2);
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertQueueEmpty();
+ mTestCallback.assertSuggestionMadeAndCommit(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getTimeZoneIds());
+ }
+
+ @Test
+ public void configChanges_enableAndDisable() {
+ ControllerImpl controllerImpl =
+ new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
+ TestEnvironment testEnvironment = new TestEnvironment(
+ mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_DISABLED);
+ controllerImpl.initialize(testEnvironment, mTestCallback);
+
+ mTestLocationTimeZoneProvider.assertIsDisabled();
+ mTestThreadingDomain.assertQueueEmpty();
+ mTestCallback.assertNoSuggestionMade();
+
+ // Now signal a config change so that geo detection is enabled.
+ testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_ENABLED);
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertNextQueueItemIsDelayed(UNCERTAINTY_DELAY);
+ mTestCallback.assertNoSuggestionMade();
+
+ // Now signal a config change so that geo detection is disabled.
+ testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_DISABLED);
+
+ mTestLocationTimeZoneProvider.assertIsDisabled();
+ mTestThreadingDomain.assertQueueEmpty();
+ mTestCallback.assertNoSuggestionMade();
+ }
+
+ @Test
+ public void configChanges_disableWithPreviousSuggestion() {
+ ControllerImpl controllerImpl =
+ new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
+ TestEnvironment testEnvironment = new TestEnvironment(
+ mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED);
+ controllerImpl.initialize(testEnvironment, mTestCallback);
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
+ mTestCallback.assertNoSuggestionMade();
+
+ // Simulate a location event being received by the provider. This should cause a suggestion
+ // to be made, and the timeout to be cleared.
+ mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertQueueEmpty();
+ mTestCallback.assertSuggestionMadeAndCommit(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds());
+
+ // Simulate the user disabling the provider.
+ testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_DISABLED);
+
+ // Because there had been a previous suggestion, the controller should withdraw it
+ // immediately to let the downstream components know that the provider can no longer be sure
+ // of the time zone.
+ mTestLocationTimeZoneProvider.assertIsDisabled();
+ mTestThreadingDomain.assertQueueEmpty();
+ mTestCallback.assertSuggestionMadeAndCommit(null);
+ }
+
+ @Test
+ public void configChanges_userSwitch_enabledToEnabled() {
+ ControllerImpl controllerImpl =
+ new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
+ TestEnvironment testEnvironment = new TestEnvironment(
+ mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED);
+ controllerImpl.initialize(testEnvironment, mTestCallback);
+
+ // There should be a runnable scheduled to suggest uncertainty if no event is received.
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
+ mTestCallback.assertNoSuggestionMade();
+
+ // Have the provider suggest a time zone.
+ mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
+
+ // Receiving a "success" provider event should cause a suggestion to be made synchronously,
+ // and also clear the scheduled uncertainty suggestion.
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertQueueEmpty();
+ mTestCallback.assertSuggestionMadeAndCommit(
+ USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds());
+
+ // Simulate the user change (but geo detection still enabled).
+ testEnvironment.simulateConfigChange(USER2_CONFIG_GEO_DETECTION_ENABLED);
+
+ // We expect the provider to end up in PROVIDER_STATE_ENABLED, but it should have been
+ // disabled when the user changed.
+ // The controller should schedule a runnable to make a suggestion if the provider doesn't
+ // send a success event.
+ int[] expectedStateTransitions = { PROVIDER_STATE_DISABLED, PROVIDER_STATE_ENABLED };
+ mTestLocationTimeZoneProvider.assertStateChangesAndCommit(expectedStateTransitions);
+ mTestLocationTimeZoneProvider.assertConfig(USER2_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
+ mTestCallback.assertNoSuggestionMade();
+
+ // Simulate no event being received, and time passing.
+ mTestThreadingDomain.executeNext();
+
+ mTestLocationTimeZoneProvider.assertIsEnabled(USER2_CONFIG_GEO_DETECTION_ENABLED);
+ mTestThreadingDomain.assertQueueEmpty();
+ mTestCallback.assertUncertainSuggestionMadeAndCommit();
+ }
+
+ private static LocationTimeZoneEvent createLocationTimeZoneEvent(@UserIdInt int userId,
+ int eventType, @Nullable List<String> timeZoneIds) {
+ LocationTimeZoneEvent.Builder builder = new LocationTimeZoneEvent.Builder()
+ .setElapsedRealtimeNanos(ARBITRARY_TIME)
+ .setUserHandle(UserHandle.of(userId))
+ .setEventType(eventType);
+ if (timeZoneIds != null) {
+ builder.setTimeZoneIds(timeZoneIds);
+ }
+ return builder.build();
+ }
+
+ private static class TestEnvironment extends LocationTimeZoneProviderController.Environment {
+
+ private final LocationTimeZoneProviderController mController;
+ private ConfigurationInternal mConfigurationInternal;
+
+ TestEnvironment(ThreadingDomain threadingDomain,
+ LocationTimeZoneProviderController controller,
+ ConfigurationInternal configurationInternal) {
+ super(threadingDomain);
+ mController = Objects.requireNonNull(controller);
+ mConfigurationInternal = Objects.requireNonNull(configurationInternal);
+ }
+
+ @Override
+ ConfigurationInternal getCurrentUserConfigurationInternal() {
+ return mConfigurationInternal;
+ }
+
+ void simulateConfigChange(ConfigurationInternal newConfig) {
+ ConfigurationInternal oldConfig = mConfigurationInternal;
+ mConfigurationInternal = Objects.requireNonNull(newConfig);
+ if (Objects.equals(oldConfig, newConfig)) {
+ fail("Bad test? No config change when one was expected");
+ }
+ mController.onConfigChanged();
+ }
+ }
+
+ private static class TestCallback extends LocationTimeZoneProviderController.Callback {
+
+ private TestState<GeolocationTimeZoneSuggestion> mLatestSuggestion = new TestState<>();
+
+ TestCallback(ThreadingDomain threadingDomain) {
+ super(threadingDomain);
+ }
+
+ @Override
+ void suggest(GeolocationTimeZoneSuggestion suggestion) {
+ mLatestSuggestion.set(suggestion);
+ }
+
+ void assertSuggestionMadeAndCommit(@Nullable List<String> expectedZoneIds) {
+ mLatestSuggestion.assertHasBeenSet();
+ assertEquals(expectedZoneIds, mLatestSuggestion.getLatest().getZoneIds());
+ mLatestSuggestion.commitLatest();
+ }
+
+ void assertNoSuggestionMade() {
+ mLatestSuggestion.assertHasNotBeenSet();
+ }
+
+ void assertUncertainSuggestionMadeAndCommit() {
+ // An "uncertain" suggestion has null time zone IDs.
+ assertSuggestionMadeAndCommit(null);
+ }
+ }
+
+ private static class TestLocationTimeZoneProvider extends LocationTimeZoneProvider {
+
+ /** Used to track historic provider states for tests. */
+ private final TestState<ProviderState> mTestProviderState = new TestState<>();
+ private boolean mInitialized;
+
+ /**
+ * Creates the instance.
+ */
+ TestLocationTimeZoneProvider(ThreadingDomain threadingDomain, String providerName) {
+ super(threadingDomain, providerName);
+ }
+
+ @Override
+ void onInitialize() {
+ mInitialized = true;
+ }
+
+ @Override
+ void onSetCurrentState(ProviderState newState) {
+ mTestProviderState.set(newState);
+ }
+
+ @Override
+ void onEnable() {
+ // Nothing needed for tests.
+ }
+
+ @Override
+ void onDisable() {
+ // Nothing needed for tests.
+ }
+
+ @Override
+ void logWarn(String msg) {
+ System.out.println(msg);
+ }
+
+ @Override
+ public void dump(IndentingPrintWriter pw, String[] args) {
+ // Nothing needed for tests.
+ }
+
+ /** Asserts that {@link #initialize(ProviderListener)} has been called. */
+ void assertInitialized() {
+ assertTrue(mInitialized);
+ }
+
+ void assertIsDisabled() {
+ // Disabled providers don't hold config.
+ assertConfig(null);
+ assertIsEnabledAndCommit(false);
+ }
+
+ /**
+ * Asserts the provider's config matches the expected, and the current state is set
+ * accordinly. Commits the latest changes to the state.
+ */
+ void assertIsEnabled(@NonNull ConfigurationInternal expectedConfig) {
+ assertConfig(expectedConfig);
+
+ boolean expectIsEnabled = expectedConfig.getAutoDetectionEnabledBehavior();
+ assertIsEnabledAndCommit(expectIsEnabled);
+ }
+
+ private void assertIsEnabledAndCommit(boolean enabled) {
+ ProviderState currentState = mCurrentState.get();
+ if (enabled) {
+ assertEquals(PROVIDER_STATE_ENABLED, currentState.stateEnum);
+ } else {
+ assertEquals(PROVIDER_STATE_DISABLED, currentState.stateEnum);
+ }
+ mTestProviderState.commitLatest();
+ }
+
+ void assertConfig(@NonNull ConfigurationInternal expectedConfig) {
+ ProviderState currentState = mCurrentState.get();
+ assertEquals(expectedConfig, currentState.currentUserConfiguration);
+ }
+
+ void simulateLocationTimeZoneEvent(@NonNull LocationTimeZoneEvent event) {
+ handleLocationTimeZoneEvent(event);
+ }
+
+ /**
+ * Asserts the most recent state changes. The ordering is such that the last element in the
+ * provided array is expected to be the current state.
+ */
+ void assertStateChangesAndCommit(int... expectedProviderStates) {
+ if (expectedProviderStates.length == 0) {
+ mTestProviderState.assertHasNotBeenSet();
+ } else {
+ mTestProviderState.assertChangeCount(expectedProviderStates.length);
+
+ List<ProviderState> previousProviderStates = new ArrayList<>();
+ for (int i = 0; i < expectedProviderStates.length; i++) {
+ previousProviderStates.add(mTestProviderState.getPrevious(i));
+ }
+ // The loop above will produce a list with the most recent state in element 0. So,
+ // reverse the list as the arguments to this method are expected to be in order
+ // oldest...latest.
+ Collections.reverse(previousProviderStates);
+
+ boolean allMatch = true;
+ for (int i = 0; i < expectedProviderStates.length; i++) {
+ allMatch = allMatch && expectedProviderStates[i]
+ == previousProviderStates.get(i).stateEnum;
+ }
+ if (!allMatch) {
+ fail("Provider state enums expected=" + Arrays.toString(expectedProviderStates)
+ + " but states were"
+ + " actually=" + previousProviderStates);
+ }
+ }
+ mTestProviderState.commitLatest();
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/HandlerThreadingDomainTest.java b/services/tests/servicestests/src/com/android/server/location/timezone/HandlerThreadingDomainTest.java
new file mode 100644
index 0000000..cbaf0f3
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/location/timezone/HandlerThreadingDomainTest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.location.timezone;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.location.timezone.ThreadingDomain.SingleRunnableQueue;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Tests for {@link HandlerThreadingDomain}. */
+@Presubmit
+public class HandlerThreadingDomainTest {
+
+ private HandlerThread mHandlerThread;
+ private Handler mTestHandler;
+
+ @Before
+ public void setUp() {
+ mHandlerThread = new HandlerThread("HandlerThreadingDomainTest");
+ mHandlerThread.start();
+ mTestHandler = new Handler(mHandlerThread.getLooper());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mHandlerThread.quit();
+ mHandlerThread.join();
+ }
+
+ @Test
+ public void getLockObject() {
+ ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler);
+ assertSame("LockObject must be consistent", domain.getLockObject(), domain.getLockObject());
+ }
+
+ @Test
+ public void assertCurrentThread() throws Exception {
+ ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler);
+
+ // Expect an exception (current thread != handler thread)
+ try {
+ domain.assertCurrentThread();
+ fail("Expected exception");
+ } catch (RuntimeException expected) {
+ // Expected
+ }
+
+ // Expect no exception (current thread == handler thread)
+ AtomicBoolean exceptionThrown = new AtomicBoolean(true);
+ LatchedRunnable testCode = new LatchedRunnable(() -> {
+ domain.assertCurrentThread();
+ exceptionThrown.set(false);
+ });
+ mTestHandler.post(testCode);
+ testCode.assertCompletesWithin(60, TimeUnit.SECONDS);
+ assertFalse(exceptionThrown.get());
+ }
+
+ @Test
+ public void post() throws Exception {
+ ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler);
+ AtomicBoolean ranOnExpectedThread = new AtomicBoolean(false);
+ LatchedRunnable testLogic = new LatchedRunnable(() -> {
+ ranOnExpectedThread.set(Thread.currentThread() == mTestHandler.getLooper().getThread());
+ });
+ domain.post(testLogic);
+ testLogic.assertCompletesWithin(60, TimeUnit.SECONDS);
+ assertTrue(ranOnExpectedThread.get());
+ }
+
+ @Test
+ public void postDelayed() throws Exception {
+ ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler);
+
+ long beforeExecutionNanos = System.nanoTime();
+ AtomicBoolean ranOnExpectedThread = new AtomicBoolean(false);
+ LatchedRunnable testLogic = new LatchedRunnable(() -> {
+ ranOnExpectedThread.set(Thread.currentThread() == mTestHandler.getLooper().getThread());
+ });
+ domain.postDelayed(testLogic, 5000);
+
+ testLogic.assertCompletesWithin(60, TimeUnit.SECONDS);
+ assertTrue(ranOnExpectedThread.get());
+
+ long afterExecutionNanos = System.nanoTime();
+ assertTrue(afterExecutionNanos - beforeExecutionNanos >= TimeUnit.SECONDS.toNanos(5));
+ }
+
+ @Test
+ public void singleRunnableHandler_runSynchronously() throws Exception {
+ ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler);
+ SingleRunnableQueue singleRunnableQueue = domain.createSingleRunnableQueue();
+
+ AtomicBoolean testPassed = new AtomicBoolean(false);
+ // Calls to SingleRunnableQueue must be made on the handler thread it is associated with,
+ // so this uses runWithScissors() to block until the lambda has completed.
+ mTestHandler.runWithScissors(() -> {
+ Thread testThread = Thread.currentThread();
+ CountDownLatch latch = new CountDownLatch(1);
+ singleRunnableQueue.runSynchronously(() -> {
+ assertSame(Thread.currentThread(), testThread);
+ latch.countDown();
+ });
+ assertTrue(awaitWithRuntimeException(latch, 60, TimeUnit.SECONDS));
+ testPassed.set(true);
+ }, TimeUnit.SECONDS.toMillis(60));
+ assertTrue(testPassed.get());
+ }
+
+ @Test
+ public void singleRunnableHandler_runDelayed() throws Exception {
+ ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler);
+ SingleRunnableQueue singleRunnableQueue = domain.createSingleRunnableQueue();
+
+ long beforeExecutionNanos = System.nanoTime();
+
+ Runnable noOpRunnable = () -> {
+ // Deliberately do nothing
+ };
+ LatchedRunnable firstRunnable = new LatchedRunnable(noOpRunnable);
+ LatchedRunnable secondRunnable = new LatchedRunnable(noOpRunnable);
+
+ // Calls to SingleRunnableQueue must be made on the handler thread it is associated with,
+ // so this uses runWithScissors() to block until the runDelayedTestRunnable has completed.
+ Runnable runDelayedTestRunnable = () -> {
+ singleRunnableQueue.runDelayed(firstRunnable, TimeUnit.SECONDS.toMillis(10));
+
+ // The second runnable posted must clear the first.
+ singleRunnableQueue.runDelayed(secondRunnable, TimeUnit.SECONDS.toMillis(10));
+ };
+ mTestHandler.runWithScissors(runDelayedTestRunnable, TimeUnit.SECONDS.toMillis(60));
+
+ // Now wait for the second runnable to complete
+ secondRunnable.assertCompletesWithin(60, TimeUnit.SECONDS);
+ assertFalse(firstRunnable.isComplete());
+
+ long afterExecutionNanos = System.nanoTime();
+ assertTrue(afterExecutionNanos - beforeExecutionNanos >= TimeUnit.SECONDS.toNanos(10));
+ }
+
+ private static boolean awaitWithRuntimeException(
+ CountDownLatch latch, long timeout, TimeUnit timeUnit) {
+ try {
+ return latch.await(timeout, timeUnit);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static class LatchedRunnable implements Runnable {
+
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+ private final Runnable mRunnable;
+
+ LatchedRunnable(Runnable mRunnable) {
+ this.mRunnable = Objects.requireNonNull(mRunnable);
+ }
+
+ @Override
+ public void run() {
+ try {
+ mRunnable.run();
+ } finally {
+ mLatch.countDown();
+ }
+ }
+
+ boolean isComplete() {
+ return mLatch.getCount() == 0;
+ }
+
+ boolean waitForCompletion(long timeout, TimeUnit unit) {
+ return awaitWithRuntimeException(mLatch, timeout, unit);
+ }
+
+ void assertCompletesWithin(long timeout, TimeUnit unit) {
+ assertTrue("Runnable did not execute in time", waitForCompletion(timeout, unit));
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/NullLocationTimeZoneProviderTest.java b/services/tests/servicestests/src/com/android/server/location/timezone/NullLocationTimeZoneProviderTest.java
new file mode 100644
index 0000000..7c882fc
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/location/timezone/NullLocationTimeZoneProviderTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.location.timezone;
+
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DISABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
+import static com.android.server.location.timezone.TestSupport.USER1_CONFIG_GEO_DETECTION_ENABLED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import android.platform.test.annotations.Presubmit;
+import android.util.IndentingPrintWriter;
+
+import com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState;
+import com.android.server.timezonedetector.ConfigurationInternal;
+import com.android.server.timezonedetector.TestState;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for {@link NullLocationTimeZoneProvider} and, indirectly, the class it extends
+ * {@link LocationTimeZoneProvider}.
+ */
+@Presubmit
+public class NullLocationTimeZoneProviderTest {
+
+ private TestThreadingDomain mTestThreadingDomain;
+
+ private TestController mTestController;
+
+ @Before
+ public void setUp() {
+ mTestThreadingDomain = new TestThreadingDomain();
+ mTestController = new TestController(mTestThreadingDomain);
+ }
+
+ @Test
+ public void initialization() {
+ String providerName = "primary";
+ NullLocationTimeZoneProvider provider =
+ new NullLocationTimeZoneProvider(mTestThreadingDomain, providerName);
+ provider.initialize(providerState -> mTestController.onProviderStateChange(providerState));
+
+ ProviderState currentState = provider.getCurrentState();
+ assertEquals(PROVIDER_STATE_DISABLED, currentState.stateEnum);
+ assertNull(currentState.currentUserConfiguration);
+ assertSame(provider, currentState.provider);
+ mTestThreadingDomain.assertQueueEmpty();
+ }
+
+ @Test
+ public void enableSchedulesPermFailure() {
+ String providerName = "primary";
+ NullLocationTimeZoneProvider provider =
+ new NullLocationTimeZoneProvider(mTestThreadingDomain, providerName);
+ provider.initialize(providerState -> mTestController.onProviderStateChange(providerState));
+
+ ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED;
+ provider.enable(config);
+
+ // The StubbedProvider should enters enabled state, but immediately schedule a runnable to
+ // switch to perm failure.
+ ProviderState currentState = provider.getCurrentState();
+ assertSame(provider, currentState.provider);
+ assertEquals(PROVIDER_STATE_ENABLED, currentState.stateEnum);
+ assertEquals(config, currentState.currentUserConfiguration);
+ mTestThreadingDomain.assertSingleImmediateQueueItem();
+ // Entering enabled() does not trigger an onProviderStateChanged() as it is requested by the
+ // controller.
+ mTestController.assertProviderChangeNotTriggered();
+
+ // Check the queued runnable causes the provider to go into perm failed state.
+ mTestThreadingDomain.executeNext();
+
+ // Entering perm failed triggers an onProviderStateChanged() as it is asynchronously
+ // triggered.
+ mTestController.assertProviderChangeTriggered(PROVIDER_STATE_PERM_FAILED);
+ }
+
+ /** A test stand-in for the {@link LocationTimeZoneProviderController}. */
+ private static class TestController extends LocationTimeZoneProviderController {
+
+ private TestState<ProviderState> mProviderState = new TestState<>();
+
+ TestController(ThreadingDomain threadingDomain) {
+ super(threadingDomain);
+ }
+
+ @Override
+ void initialize(Environment environment, Callback callback) {
+ // Not needed for provider testing.
+ }
+
+ @Override
+ void onConfigChanged() {
+ // Not needed for provider testing.
+ }
+
+ void onProviderStateChange(ProviderState providerState) {
+ this.mProviderState.set(providerState);
+ }
+
+ @Override
+ public void dump(IndentingPrintWriter pw, String[] args) {
+ // Not needed for provider testing.
+ }
+
+ void assertProviderChangeTriggered(int expectedStateEnum) {
+ assertEquals(expectedStateEnum, mProviderState.getLatest().stateEnum);
+ mProviderState.commitLatest();
+ }
+
+ public void assertProviderChangeNotTriggered() {
+ mProviderState.assertHasNotBeenSet();
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/TestSupport.java b/services/tests/servicestests/src/com/android/server/location/timezone/TestSupport.java
new file mode 100644
index 0000000..192ade7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/location/timezone/TestSupport.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.location.timezone;
+
+import android.annotation.UserIdInt;
+
+import com.android.server.timezonedetector.ConfigurationInternal;
+
+/** Shared test support code for this package. */
+final class TestSupport {
+ static final @UserIdInt int USER1_ID = 9999;
+
+ static final ConfigurationInternal USER1_CONFIG_GEO_DETECTION_ENABLED =
+ createUserConfig(USER1_ID, true);
+
+ static final ConfigurationInternal USER1_CONFIG_GEO_DETECTION_DISABLED =
+ createUserConfig(USER1_ID, false);
+
+ static final @UserIdInt int USER2_ID = 1234567890;
+
+ static final ConfigurationInternal USER2_CONFIG_GEO_DETECTION_ENABLED =
+ createUserConfig(USER2_ID, true);
+
+ static final ConfigurationInternal USER2_CONFIG_GEO_DETECTION_DISABLED =
+ createUserConfig(USER2_ID, false);
+
+ private TestSupport() {
+ }
+
+ private static ConfigurationInternal createUserConfig(
+ @UserIdInt int userId, boolean geoDetectionEnabled) {
+ return new ConfigurationInternal.Builder(userId)
+ .setUserConfigAllowed(true)
+ .setAutoDetectionSupported(true)
+ .setAutoDetectionEnabled(true)
+ .setLocationEnabled(true)
+ .setGeoDetectionEnabled(geoDetectionEnabled)
+ .build();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/TestThreadingDomain.java b/services/tests/servicestests/src/com/android/server/location/timezone/TestThreadingDomain.java
new file mode 100644
index 0000000..70ff22d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/location/timezone/TestThreadingDomain.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.location.timezone;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.time.Duration;
+import java.util.LinkedList;
+import java.util.Objects;
+
+/**
+ * A ThreadingDomain that simulates idealized post() semantics. Execution takes place in zero time,
+ * exactly when scheduled, when the test code explicitly requests it. Execution takes place on the
+ * test's main thread.
+ */
+class TestThreadingDomain extends ThreadingDomain {
+
+ static class QueuedRunnable {
+ @NonNull public final Runnable runnable;
+ @Nullable public final Object token;
+ public final long executionTimeMillis;
+
+ QueuedRunnable(@NonNull Runnable runnable, @Nullable Object token,
+ long executionTimeMillis) {
+ this.runnable = Objects.requireNonNull(runnable);
+ this.token = token;
+ this.executionTimeMillis = executionTimeMillis;
+ }
+
+ @Override
+ public String toString() {
+ return "QueuedRunnable{"
+ + "runnable=" + runnable
+ + ", token=" + token
+ + ", executionTimeMillis=" + executionTimeMillis
+ + '}';
+ }
+ }
+
+ private long mCurrentTimeMillis;
+ private LinkedList<QueuedRunnable> mQueue = new LinkedList<>();
+
+ TestThreadingDomain() {
+ // Pick an arbitrary time.
+ mCurrentTimeMillis = 123456L;
+ }
+
+ @Override
+ Thread getThread() {
+ return Thread.currentThread();
+ }
+
+ @Override
+ void post(Runnable r) {
+ mQueue.add(new QueuedRunnable(r, null, mCurrentTimeMillis));
+ }
+
+ @Override
+ void postDelayed(Runnable r, long delayMillis) {
+ mQueue.add(new QueuedRunnable(r, null, mCurrentTimeMillis + delayMillis));
+ }
+
+ @Override
+ void postDelayed(Runnable r, Object token, long delayMillis) {
+ mQueue.add(new QueuedRunnable(r, token, mCurrentTimeMillis + delayMillis));
+ }
+
+ @Override
+ void removeQueuedRunnables(Object token) {
+ mQueue.removeIf(runnable -> runnable.token != null && runnable.token.equals(token));
+ }
+
+ void assertSingleDelayedQueueItem(Duration expectedDelay) {
+ assertQueueLength(1);
+ assertNextQueueItemIsDelayed(expectedDelay);
+ }
+
+ void assertSingleImmediateQueueItem() {
+ assertQueueLength(1);
+ assertNextQueueItemIsImmediate();
+ }
+
+ void assertQueueLength(int expectedLength) {
+ assertEquals(expectedLength, mQueue.size());
+ }
+
+ void assertNextQueueItemIsImmediate() {
+ assertTrue(getNextQueueItemDelayMillis() == 0);
+ }
+
+ void assertNextQueueItemIsDelayed(Duration expectedDelay) {
+ assertTrue(getNextQueueItemDelayMillis() == expectedDelay.toMillis());
+ }
+
+ void assertQueueEmpty() {
+ assertTrue(mQueue.isEmpty());
+ }
+
+ long getNextQueueItemDelayMillis() {
+ assertQueueLength(1);
+ return mQueue.getFirst().executionTimeMillis - mCurrentTimeMillis;
+ }
+
+ void executeNext() {
+ assertQueueLength(1);
+
+ QueuedRunnable queued = mQueue.removeFirst();
+ mCurrentTimeMillis = queued.executionTimeMillis;
+ queued.runnable.run();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TestState.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TestState.java
index 7049efa1..97b8360 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/TestState.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TestState.java
@@ -76,7 +76,7 @@
/** Asserts the number of times {@link #set} has been called. */
public void assertChangeCount(int expectedCount) {
- assertEquals(expectedCount, mValues.size());
+ assertEquals(expectedCount, getChangeCount());
}
/**
@@ -89,4 +89,25 @@
}
return mInitialValue;
}
+
+ /** Returns the number of times {@link #set} has been called. */
+ public int getChangeCount() {
+ return mValues.size();
+ }
+
+ /**
+ * Returns an historic value of the state. Values for {@code age} can be from {@code 0}, the
+ * latest value, through {@code getChangeCount() - 1}, which returns the oldest change, to
+ * {@code getChangeCount()}, which returns the initial value. Values outside of this range will
+ * cause {@link IndexOutOfBoundsException} to be thrown.
+ */
+ public T getPrevious(int age) {
+ int size = mValues.size();
+ if (age < size) {
+ return mValues.get(size - 1 - age);
+ } else if (age == size) {
+ return mInitialValue;
+ }
+ throw new IndexOutOfBoundsException("age=" + age + " is too big.");
+ }
}
diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp
index a53ea16..d430db5 100644
--- a/tests/FlickerTests/Android.bp
+++ b/tests/FlickerTests/Android.bp
@@ -29,7 +29,8 @@
"flickerlib",
"truth-prebuilt",
"launcher-helper-lib",
- "launcher-aosp-tapl"
+ "launcher-aosp-tapl",
+ "platform-test-annotations",
],
}
@@ -50,6 +51,7 @@
"truth-prebuilt",
"app-helpers-core",
"launcher-helper-lib",
- "launcher-aosp-tapl"
+ "launcher-aosp-tapl",
+ "platform-test-annotations",
],
}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt
index b64811b..c1ba21a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt
@@ -16,6 +16,7 @@
package com.android.server.wm.flicker.ime
+import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.dsl.flicker
@@ -39,6 +40,7 @@
* Test IME window closing back to app window transitions.
* To run this test: `atest FlickerTests:CloseImeWindowToAppTest`
*/
+@Presubmit
@RequiresDevice
@RunWith(Parameterized::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
index 0940c19..2c00722 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
@@ -16,6 +16,7 @@
package com.android.server.wm.flicker.ime
+import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.NonRotationTestBase
@@ -39,6 +40,7 @@
* Test IME window closing back to app window transitions.
* To run this test: `atest FlickerTests:CloseImeWindowToAppTest`
*/
+@Presubmit
@RequiresDevice
@RunWith(Parameterized::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
index c2e87db..4697adc 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
@@ -16,6 +16,7 @@
package com.android.server.wm.flicker.ime
+import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.NonRotationTestBase
@@ -40,6 +41,7 @@
* Test IME window closing to home transitions.
* To run this test: `atest FlickerTests:CloseImeWindowToHomeTest`
*/
+@Presubmit
@RequiresDevice
@RunWith(Parameterized::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt
index 11ccb69..2caa8f3 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt
@@ -16,6 +16,7 @@
package com.android.server.wm.flicker.ime
+import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.NonRotationTestBase
@@ -39,6 +40,7 @@
* Test IME window opening transitions.
* To run this test: `atest FlickerTests:OpenImeWindowTest`
*/
+@Presubmit
@RequiresDevice
@RunWith(Parameterized::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt
index 1759072..2c9c8ba 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt
@@ -16,6 +16,7 @@
package com.android.server.wm.flicker.launch
+import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.dsl.flicker
@@ -38,6 +39,7 @@
* Test cold launch app from launcher.
* To run this test: `atest FlickerTests:OpenAppColdTest`
*/
+@Presubmit
@RequiresDevice
@RunWith(Parameterized::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/splitscreen/SplitScreenToLauncherTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/splitscreen/SplitScreenToLauncherTest.kt
index 87c8633..7447bda 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/splitscreen/SplitScreenToLauncherTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/splitscreen/SplitScreenToLauncherTest.kt
@@ -16,6 +16,7 @@
package com.android.server.wm.flicker.splitscreen
+import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.NonRotationTestBase
@@ -43,6 +44,7 @@
* Test open app to split screen.
* To run this test: `atest FlickerTests:SplitScreenToLauncherTest`
*/
+@Presubmit
@RequiresDevice
@RunWith(Parameterized::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)