diff --git a/core/java/android/app/timezonedetector/ManualTimeZoneSuggestion.java b/core/java/android/app/timezonedetector/ManualTimeZoneSuggestion.java index 3a9adc72aab16..22e2efb1deddb 100644 --- a/core/java/android/app/timezonedetector/ManualTimeZoneSuggestion.java +++ b/core/java/android/app/timezonedetector/ManualTimeZoneSuggestion.java @@ -20,7 +20,9 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; +import android.os.ShellCommand; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -127,4 +129,32 @@ public final class ManualTimeZoneSuggestion implements Parcelable { + ", mDebugInfo=" + mDebugInfo + '}'; } + + /** @hide */ + public static ManualTimeZoneSuggestion parseCommandLineArg(@NonNull ShellCommand cmd) { + String zoneId = null; + String opt; + while ((opt = cmd.getNextArg()) != null) { + switch (opt) { + case "--zone_id": { + zoneId = cmd.getNextArgRequired(); + break; + } + default: { + throw new IllegalArgumentException("Unknown option: " + opt); + } + } + } + ManualTimeZoneSuggestion suggestion = new ManualTimeZoneSuggestion(zoneId); + suggestion.addDebugInfo("Command line injection"); + return suggestion; + } + + /** @hide */ + public static void printCommandLineOpts(@NonNull PrintWriter pw) { + pw.println("Manual suggestion options:"); + pw.println(" --zone_id "); + pw.println(); + pw.println("See " + ManualTimeZoneSuggestion.class.getName() + " for more information"); + } } diff --git a/core/java/android/app/timezonedetector/TelephonyTimeZoneSuggestion.java b/core/java/android/app/timezonedetector/TelephonyTimeZoneSuggestion.java index 150c01d598999..430462b07c7a8 100644 --- a/core/java/android/app/timezonedetector/TelephonyTimeZoneSuggestion.java +++ b/core/java/android/app/timezonedetector/TelephonyTimeZoneSuggestion.java @@ -21,7 +21,10 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; +import android.os.ShellCommand; +import android.text.TextUtils; +import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -392,4 +395,96 @@ public final class TelephonyTimeZoneSuggestion implements Parcelable { return new TelephonyTimeZoneSuggestion(this); } } + + /** @hide */ + public static TelephonyTimeZoneSuggestion parseCommandLineArg(@NonNull ShellCommand cmd) + throws IllegalArgumentException { + Integer slotIndex = null; + String zoneId = null; + Integer quality = null; + Integer matchType = null; + String opt; + while ((opt = cmd.getNextArg()) != null) { + switch (opt) { + case "--slot_index": { + slotIndex = Integer.parseInt(cmd.getNextArgRequired()); + break; + } + case "--zone_id": { + zoneId = cmd.getNextArgRequired(); + break; + } + case "--quality": { + quality = parseQualityCommandLineArg(cmd.getNextArgRequired()); + break; + } + case "--match_type": { + matchType = parseMatchTypeCommandLineArg(cmd.getNextArgRequired()); + break; + } + default: { + throw new IllegalArgumentException("Unknown option: " + opt); + } + } + } + + if (slotIndex == null) { + throw new IllegalArgumentException("No slotIndex specified."); + } + + Builder builder = new Builder(slotIndex); + if (!(TextUtils.isEmpty(zoneId) || "_".equals(zoneId))) { + builder.setZoneId(zoneId); + } + if (quality != null) { + builder.setQuality(quality); + } + if (matchType != null) { + builder.setMatchType(matchType); + } + builder.addDebugInfo("Command line injection"); + return builder.build(); + } + + private static int parseQualityCommandLineArg(@NonNull String arg) { + switch (arg) { + case "single": + return QUALITY_SINGLE_ZONE; + case "multiple_same": + return QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET; + case "multiple_different": + return QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS; + default: + throw new IllegalArgumentException("Unrecognized quality: " + arg); + } + } + + private static int parseMatchTypeCommandLineArg(@NonNull String arg) { + switch (arg) { + case "emulator": + return MATCH_TYPE_EMULATOR_ZONE_ID; + case "country_with_offset": + return MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET; + case "country": + return MATCH_TYPE_NETWORK_COUNTRY_ONLY; + case "test_network": + return MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY; + default: + throw new IllegalArgumentException("Unrecognized match_type: " + arg); + } + } + + /** @hide */ + public static void printCommandLineOpts(@NonNull PrintWriter pw) { + pw.println("Telephony suggestion options:"); + pw.println(" --slot_index "); + pw.println(" To withdraw a previous suggestion:"); + pw.println(" [--zone_id \"_\"]"); + pw.println(" To make a new suggestion:"); + pw.println(" --zone_id "); + pw.println(" --quality "); + pw.println(" --match_type "); + pw.println(); + pw.println("See " + TelephonyTimeZoneSuggestion.class.getName() + " for more information"); + } } diff --git a/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java b/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java index 02ed0edd87fb6..17838bb19a8fe 100644 --- a/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java +++ b/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java @@ -18,12 +18,19 @@ package android.app.timezonedetector; import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable; +import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import android.os.ShellCommand; import org.junit.Test; +import java.io.PrintWriter; +import java.io.StringWriter; + public class ManualTimeZoneSuggestionTest { private static final String ARBITRARY_ZONE_ID1 = "Europe/London"; @@ -58,4 +65,36 @@ public class ManualTimeZoneSuggestionTest { ManualTimeZoneSuggestion rtSuggestion = roundTripParcelable(suggestion); assertEquals(suggestion.getDebugInfo(), rtSuggestion.getDebugInfo()); } + + @Test + public void testPrintCommandLineOpts() throws Exception { + try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) { + ManualTimeZoneSuggestion.printCommandLineOpts(pw); + assertTrue(sw.getBuffer().length() > 0); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testParseCommandLineArg_noArgs() { + ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(""); + ManualTimeZoneSuggestion.parseCommandLineArg(testShellCommand); + } + + @Test + public void testParseCommandLineArg_validSuggestion() { + ShellCommand testShellCommand = + createShellCommandWithArgsAndOptions("--zone_id Europe/London"); + ManualTimeZoneSuggestion expectedSuggestion = + new ManualTimeZoneSuggestion("Europe/London"); + ManualTimeZoneSuggestion actualSuggestion = + ManualTimeZoneSuggestion.parseCommandLineArg(testShellCommand); + assertEquals(expectedSuggestion, actualSuggestion); + } + + @Test(expected = IllegalArgumentException.class) + public void testParseCommandLineArg_unknownArgument() { + ShellCommand testShellCommand = createShellCommandWithArgsAndOptions( + "--zone_id Europe/London --bad_arg 0"); + ManualTimeZoneSuggestion.parseCommandLineArg(testShellCommand); + } } diff --git a/core/tests/coretests/src/android/app/timezonedetector/ShellCommandTestSupport.java b/core/tests/coretests/src/android/app/timezonedetector/ShellCommandTestSupport.java new file mode 100644 index 0000000000000..8d8290c7bdc9c --- /dev/null +++ b/core/tests/coretests/src/android/app/timezonedetector/ShellCommandTestSupport.java @@ -0,0 +1,65 @@ +/* + * Copyright 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 android.app.timezonedetector; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.os.ShellCommand; + +import org.mockito.stubbing.Answer; + +import java.util.Arrays; +import java.util.List; + +/** Utility methods related to {@link ShellCommand} objects used in several tests. */ +final class ShellCommandTestSupport { + private ShellCommandTestSupport() {} + + static ShellCommand createShellCommandWithArgsAndOptions(String argsWithSpaces) { + return createShellCommandWithArgsAndOptions(Arrays.asList(argsWithSpaces.split(" "))); + } + + static ShellCommand createShellCommandWithArgsAndOptions(List args) { + ShellCommand command = mock(ShellCommand.class); + class ArgProvider { + private int mCount; + + String getNext() { + if (mCount >= args.size()) { + return null; + } + return args.get(mCount++); + } + + String getNextRequired() { + String next = getNext(); + if (next == null) { + throw new IllegalArgumentException("No next"); + } + return next; + } + } + ArgProvider argProvider = new ArgProvider(); + when(command.getNextArg()).thenAnswer( + (Answer) invocation -> argProvider.getNext()); + when(command.getNextOption()).thenAnswer( + (Answer) invocation -> argProvider.getNext()); + when(command.getNextArgRequired()).thenAnswer( + (Answer) invocation -> argProvider.getNextRequired()); + return command; + } +} diff --git a/core/tests/coretests/src/android/app/timezonedetector/TelephonyTimeZoneSuggestionTest.java b/core/tests/coretests/src/android/app/timezonedetector/TelephonyTimeZoneSuggestionTest.java index 59d55b79157ce..c4ff9beec0c54 100644 --- a/core/tests/coretests/src/android/app/timezonedetector/TelephonyTimeZoneSuggestionTest.java +++ b/core/tests/coretests/src/android/app/timezonedetector/TelephonyTimeZoneSuggestionTest.java @@ -18,13 +18,19 @@ package android.app.timezonedetector; import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable; +import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import android.os.ShellCommand; + import org.junit.Test; +import java.io.PrintWriter; +import java.io.StringWriter; + public class TelephonyTimeZoneSuggestionTest { private static final int SLOT_INDEX = 99999; @@ -159,4 +165,57 @@ public class TelephonyTimeZoneSuggestionTest { assertEquals(suggestion1, suggestion1_2); assertTrue(suggestion1_2.getDebugInfo().contains(debugString)); } + + @Test + public void testPrintCommandLineOpts() throws Exception { + try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) { + TelephonyTimeZoneSuggestion.printCommandLineOpts(pw); + assertTrue(sw.getBuffer().length() > 0); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testParseCommandLineArg_noArgs() { + ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(""); + TelephonyTimeZoneSuggestion.parseCommandLineArg(testShellCommand); + } + + @Test(expected = IllegalArgumentException.class) + public void testParseCommandLineArg_noSlotIndex() { + ShellCommand testShellCommand = createShellCommandWithArgsAndOptions("--zone_id _"); + TelephonyTimeZoneSuggestion.parseCommandLineArg(testShellCommand); + } + + @Test + public void testParseCommandLineArg_validEmptyZoneIdSuggestion() { + ShellCommand testShellCommand = createShellCommandWithArgsAndOptions( + "--slot_index 0 --zone_id _"); + TelephonyTimeZoneSuggestion expectedSuggestion = + new TelephonyTimeZoneSuggestion.Builder(0).build(); + TelephonyTimeZoneSuggestion actualSuggestion = + TelephonyTimeZoneSuggestion.parseCommandLineArg(testShellCommand); + assertEquals(expectedSuggestion, actualSuggestion); + } + + @Test + public void testParseCommandLineArg_validNonEmptySuggestion() { + ShellCommand testShellCommand = createShellCommandWithArgsAndOptions( + "--slot_index 0 --zone_id Europe/London --quality single --match_type country"); + TelephonyTimeZoneSuggestion expectedSuggestion = + new TelephonyTimeZoneSuggestion.Builder(0) + .setZoneId("Europe/London") + .setQuality(TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE) + .setMatchType(TelephonyTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_ONLY) + .build(); + TelephonyTimeZoneSuggestion actualSuggestion = + TelephonyTimeZoneSuggestion.parseCommandLineArg(testShellCommand); + assertEquals(expectedSuggestion, actualSuggestion); + } + + @Test(expected = IllegalArgumentException.class) + public void testParseCommandLineArg_unknownArgument() { + ShellCommand testShellCommand = createShellCommandWithArgsAndOptions( + "--slot_index 0 --zone_id _ --bad_arg 0"); + TelephonyTimeZoneSuggestion.parseCommandLineArg(testShellCommand); + } } diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java index 57b6ec9062a8b..fc5258403685b 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java @@ -25,6 +25,8 @@ import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; import android.os.Handler; +import android.os.ResultReceiver; +import android.os.ShellCallback; import android.provider.Settings; import com.android.internal.annotations.VisibleForTesting; @@ -133,5 +135,13 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub android.Manifest.permission.SUGGEST_MANUAL_TIME_AND_ZONE, "suggest manual time and time zone"); } + + @Override + public void onShellCommand(FileDescriptor in, FileDescriptor out, + FileDescriptor err, String[] args, ShellCallback callback, + ResultReceiver resultReceiver) { + (new TimeZoneDetectorShellCommand(this)).exec( + this, in, out, err, args, callback, resultReceiver); + } } diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java new file mode 100644 index 0000000000000..b051bab71f12e --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.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.timezonedetector; + +import android.app.timezonedetector.ManualTimeZoneSuggestion; +import android.app.timezonedetector.TelephonyTimeZoneSuggestion; +import android.os.ShellCommand; + +import java.io.PrintWriter; + +/** Implemented the shell command interface for {@link TimeZoneDetectorService}. */ +class TimeZoneDetectorShellCommand extends ShellCommand { + + private final TimeZoneDetectorService mInterface; + + TimeZoneDetectorShellCommand(TimeZoneDetectorService timeZoneDetectorService) { + mInterface = timeZoneDetectorService; + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + + switch (cmd) { + case "suggestTelephonyTimeZone": + return runSuggestTelephonyTimeZone(); + case "suggestManualTimeZone": + return runSuggestManualTimeZone(); + default: { + return handleDefaultCommands(cmd); + } + } + } + + private int runSuggestTelephonyTimeZone() { + final PrintWriter pw = getOutPrintWriter(); + try { + TelephonyTimeZoneSuggestion suggestion = null; + String opt; + while ((opt = getNextArg()) != null) { + if ("--suggestion".equals(opt)) { + suggestion = TelephonyTimeZoneSuggestion.parseCommandLineArg(this); + } else { + pw.println("Error: Unknown option: " + opt); + return 1; + } + } + if (suggestion == null) { + pw.println("Error: suggestion not specified"); + return 1; + } + mInterface.suggestTelephonyTimeZone(suggestion); + pw.println("Suggestion " + suggestion + " injected."); + return 0; + } catch (RuntimeException e) { + pw.println(e.toString()); + return 1; + } + } + + private int runSuggestManualTimeZone() { + final PrintWriter pw = getOutPrintWriter(); + try { + ManualTimeZoneSuggestion suggestion = null; + String opt; + while ((opt = getNextArg()) != null) { + if ("--suggestion".equals(opt)) { + suggestion = ManualTimeZoneSuggestion.parseCommandLineArg(this); + } else { + pw.println("Error: Unknown option: " + opt); + return 1; + } + } + if (suggestion == null) { + pw.println("Error: suggestion not specified"); + return 1; + } + mInterface.suggestManualTimeZone(suggestion); + pw.println("Suggestion " + suggestion + " injected."); + return 0; + } catch (RuntimeException e) { + pw.println(e.toString()); + return 1; + } + } + + @Override + public void onHelp() { + final PrintWriter pw = getOutPrintWriter(); + pw.println("Time Zone Detector (time_zone_detector) commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(" suggestTelephonyTimeZone"); + pw.println(" --suggestion "); + pw.println(" suggestManualTimeZone"); + pw.println(" --suggestion "); + pw.println(); + ManualTimeZoneSuggestion.printCommandLineOpts(pw); + pw.println(); + TelephonyTimeZoneSuggestion.printCommandLineOpts(pw); + pw.println(); + } +}