Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 591d4fd9 authored by Jaewan Kim's avatar Jaewan Kim
Browse files

Enable Linux terminal app via developer settings

Bug: 369246712
Test: `atest SettingsRoboTests`, try toggle the option, \
  and try search with 'Linux'.
Flag: RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES
Change-Id: Ibdcf00de04f09fe5b03ba53857904945354ed844
parent e61c0364
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -839,4 +839,7 @@

    <!-- Package name for diagnostics app. -->
    <string name="config_device_diagnostics_package_name" translatable="false">com.android.devicediagnostics</string>

    <!-- Package name for Linux terminal app -->
    <string name="config_linux_terminal_app_package_name" translatable="false">com.android.virtualization.terminal</string>
</resources>
+5 −0
Original line number Diff line number Diff line
@@ -199,6 +199,11 @@
            android:title="@string/enable_terminal_title"
            android:summary="@string/enable_terminal_summary" />

        <SwitchPreferenceCompat
            android:key="enable_linux_terminal"
            android:title="@string/enable_linux_terminal_title"
            android:summary="@string/enable_linux_terminal_summary" />

        <SwitchPreferenceCompat
            android:key="bugreport_in_power"
            android:title="@string/bugreport_in_power"
+1 −0
Original line number Diff line number Diff line
@@ -676,6 +676,7 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra
        controllers.add(new WirelessDebuggingPreferenceController(context, lifecycle));
        controllers.add(new AdbAuthorizationTimeoutPreferenceController(context));
        controllers.add(new LocalTerminalPreferenceController(context));
        controllers.add(new LinuxTerminalPreferenceController(context));
        controllers.add(new BugReportInPowerPreferenceController(context));
        controllers.add(new AutomaticSystemServerHeapDumpPreferenceController(context));
        controllers.add(new MockLocationAppPreferenceController(context, fragment));
+127 −0
Original line number Diff line number Diff line
/*
 * Copyright 2024 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.settings.development;

import android.content.Context;
import android.content.pm.PackageManager;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import androidx.preference.TwoStatePreference;

import com.android.settings.R;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settingslib.development.DeveloperOptionsPreferenceController;

public class LinuxTerminalPreferenceController extends DeveloperOptionsPreferenceController
        implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin {
    private static final String TAG = "LinuxTerminalPrefCtrl";

    private static final String ENABLE_TERMINAL_KEY = "enable_linux_terminal";

    @NonNull
    private final PackageManager mPackageManager;

    @Nullable
    private final String mTerminalPackageName;

    public LinuxTerminalPreferenceController(@NonNull Context context) {
        super(context);
        mPackageManager = mContext.getPackageManager();

        String packageName = mContext.getString(R.string.config_linux_terminal_app_package_name);
        mTerminalPackageName =
                isPackageInstalled(mPackageManager, packageName) ? packageName : null;

        Log.d(TAG, "Terminal app package name=" + packageName + ", isAvailable=" + isAvailable());
    }

    // Avoid lazy initialization because this may be called before displayPreference().
    @Override
    public boolean isAvailable() {
        // Returns true only if the terminal app is installed which only happens when the build flag
        // RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES is true.
        // TODO(b/343795511): Add explicitly check for the flag when it's accessible from Java code.
        return getTerminalPackageName() != null;
    }

    @Override
    @NonNull
    public String getPreferenceKey() {
        return ENABLE_TERMINAL_KEY;
    }

    @Override
    public void displayPreference(@NonNull PreferenceScreen screen) {
        super.displayPreference(screen);
        mPreference.setEnabled(isAvailable());
    }

    @Override
    public boolean onPreferenceChange(
                @NonNull Preference preference, @NonNull Object newValue) {
        String packageName = getTerminalPackageName();
        if (packageName == null) {
            return false;
        }

        boolean terminalEnabled = (Boolean) newValue;
        int state = terminalEnabled
                ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
                : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
        mPackageManager.setApplicationEnabledSetting(packageName, state, /* flags=*/ 0);
        ((TwoStatePreference) mPreference).setChecked(terminalEnabled);
        return true;
    }

    @Override
    public void updateState(@NonNull Preference preference) {
        String packageName = getTerminalPackageName();
        if (packageName == null) {
            return;
        }

        boolean isTerminalEnabled = mPackageManager.getApplicationEnabledSetting(packageName)
                == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
        ((TwoStatePreference) mPreference).setChecked(isTerminalEnabled);
    }

    // Can be mocked for testing
    @VisibleForTesting
    @Nullable
    String getTerminalPackageName() {
        return mTerminalPackageName;
    }

    private static boolean isPackageInstalled(PackageManager manager, String packageName) {
        if (TextUtils.isEmpty(packageName)) {
            return false;
        }
        try {
            return manager.getPackageInfo(
                    packageName,
                    PackageManager.MATCH_ALL | PackageManager.MATCH_DISABLED_COMPONENTS) != null;
        } catch (PackageManager.NameNotFoundException e) {
            return false;
        }
    }
}
+131 −0
Original line number Diff line number Diff line
/*
 * Copyright 2024 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.settings.development;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;

import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;

@RunWith(RobolectricTestRunner.class)
public class LinuxTerminalPreferenceControllerTest {

    @Mock
    private Context mContext;
    @Mock
    private SwitchPreference mPreference;
    @Mock
    private PreferenceScreen mPreferenceScreen;
    @Mock
    private PackageManager mPackageManager;
    @Mock
    private ApplicationInfo mApplicationInfo;

    private String mTerminalPackageName = "com.android.virtualization.terminal";
    private LinuxTerminalPreferenceController mController;

    @Before
    public void setup() throws Exception {
        MockitoAnnotations.initMocks(this);
        doReturn(mPackageManager).when(mContext).getPackageManager();
        doReturn(mApplicationInfo).when(mPackageManager).getApplicationInfo(
                eq(mTerminalPackageName), any());

        mController = spy(new LinuxTerminalPreferenceController(mContext));
        doReturn(true).when(mController).isAvailable();
        doReturn(mTerminalPackageName).when(mController).getTerminalPackageName();
        when(mPreferenceScreen.findPreference(mController.getPreferenceKey()))
                .thenReturn(mPreference);
        mController.displayPreference(mPreferenceScreen);
    }

    @Test
    public void isAvailable_whenPackageNameIsNull_returnsFalse() throws Exception {
        mController = spy(new LinuxTerminalPreferenceController(mContext));
        doReturn(null).when(mController).getTerminalPackageName();

        assertThat(mController.isAvailable()).isFalse();
    }

    @Test
    public void isAvailable_whenAppDoesNotExist_returnsFalse() throws Exception {
        doThrow(new NameNotFoundException()).when(mPackageManager).getApplicationInfo(
                eq(mTerminalPackageName), any());

        mController = spy(new LinuxTerminalPreferenceController(mContext));

        assertThat(mController.isAvailable()).isFalse();
    }

    @Test
    public void onPreferenceChanged_turnOnTerminal() {
        mController.onPreferenceChange(null, true);

        verify(mPackageManager).setApplicationEnabledSetting(
                mTerminalPackageName,
                PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                /* flags= */ 0);
    }

    @Test
    public void onPreferenceChanged_turnOffTerminal() {
        mController.onPreferenceChange(null, false);

        verify(mPackageManager).setApplicationEnabledSetting(
                mTerminalPackageName,
                PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
                /* flags= */ 0);
    }

    @Test
    public void updateState_preferenceShouldBeChecked() {
        when(mPackageManager.getApplicationEnabledSetting(mTerminalPackageName))
                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
        mController.updateState(mPreference);

        verify(mPreference).setChecked(true);
    }

    @Test
    public void updateState_preferenceShouldNotBeChecked() {
        when(mPackageManager.getApplicationEnabledSetting(mTerminalPackageName))
                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT);
        mController.updateState(mPreference);

        verify(mPreference).setChecked(false);
    }
}