TestArgsContentProvider.java

/*
 * Copyright (C) 2019 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 androidx.test.services.storage.provider;

import static androidx.test.internal.util.Checks.checkNotNull;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import androidx.test.services.storage.TestStorageConstants;
import androidx.test.services.storage.TestStorageServiceProto.TestArgument;
import androidx.test.services.storage.TestStorageServiceProto.TestArguments;
import androidx.test.services.storage.file.PropertyFile;
import androidx.test.services.storage.file.PropertyFile.Authority;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Provides access to test arguments stored as a file on external device storage. This provider
 * supports only the query api. Use {@link PropertyFile#buildUri(Authority)} to retrieve all test
 * arguments. To retrieve a specific argument, build the URI with the arg name as path by calling
 * {@link PropertyFile#buildUri(Authority, String)}.
 */
@SuppressWarnings("javadoc")
public final class TestArgsContentProvider extends ContentProvider {

  private static final String TAG = "TestArgCP";
  private static final String ANDROID_TEST_SERVER_SPEC_FORMAT = "_server_address";

  private static final String SYSTEM_PROPERTY_CLAZZ = "android.os.SystemProperties";
  private static final String GET_METHOD = "get";

  private String systemPropertyClassName;
  private Method getString;

  void setSystemPropertyClassNameForTest(String className) {
    this.systemPropertyClassName = className;
  }

  private String getQemuHost() {
    try {
      if (null == getString) {
        if (null == systemPropertyClassName) {
          systemPropertyClassName = SYSTEM_PROPERTY_CLAZZ;
        }

        Class<?> clazz = Class.forName(systemPropertyClassName);
        getString = clazz.getMethod(GET_METHOD, String.class, String.class);
      }
      return (String) getString.invoke(null, "qemu.host.hostname", "");
    } catch (ClassNotFoundException cnfe) {
      Log.w(TAG, "Couldn't access SysProps for qemu hostname.", cnfe);
      return "";
    } catch (SecurityException se) {
      Log.w(TAG, "Couldn't access SysProps for qemu hostname.", se);
      return "";
    } catch (NoSuchMethodException nsme) {
      Log.w(TAG, "Couldn't access SysProps for qemu hostname.", nsme);
      return "";
    } catch (InvocationTargetException ite) {
      Log.w(TAG, "Couldn't access SysProps for qemu hostname.", ite);
      return "";
    } catch (IllegalAccessException iae) {
      Log.w(TAG, "Couldn't access SysProps for qemu hostname.", iae);
      return "";
    } catch (IllegalArgumentException iae) {
      Log.w(TAG, "Couldn't access SysProps for qemu hostname.", iae);
      return "";
    }
  }

  @Override
  public int delete(Uri arg0, String arg1, String[] arg2) {
    // Not allowed.
    return 0;
  }

  @Override
  public String getType(Uri arg0) {
    // mime types not supported
    return null;
  }

  @Override
  public Uri insert(Uri arg0, ContentValues arg1) {
    throw new UnsupportedOperationException("Insertion is not allowed.");
  }

  @Override
  public boolean onCreate() {
    return true;
  }

  @Override
  public Cursor query(
      Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    checkNotNull(uri);

    Map<String, String> argMap = buildArgMapFromFile();

    MatrixCursor cursor = new MatrixCursor(PropertyFile.Column.getNames());
    String argName = uri.getLastPathSegment();
    if (argName != null) {
      // Return the specific arg name/value.
      if (argMap.containsKey(argName)) {
        String[] row = {argName, argMap.get(argName)};
        cursor.addRow(row);
      }
    } else {
      // No specific arg specified. Return the entire argMap.
      for (Entry<String, String> entry : argMap.entrySet()) {
        String[] row = {entry.getKey(), entry.getValue()};
        cursor.addRow(row);
      }
    }
    return cursor;
  }

  private Map<String, String> buildArgMapFromFile() {
    Map<String, String> cleanArgMap = new HashMap<>();
    Map<String, String> qemuArgMap = new HashMap<>();
    String qemuHost = getQemuHost();

    for (TestArgument testArg : readProtoFromFile().getArgList()) {
      String key = testArg.getName();
      String val = testArg.getValue();
      cleanArgMap.put(key, val);

      if (!"".equals(qemuHost) && key.endsWith(ANDROID_TEST_SERVER_SPEC_FORMAT)) {
        String serverHost = val.split(":")[0];
        if (serverHost.startsWith(qemuHost)) {
          // TODO: remove startswith check once the emulator launcher passes in FQDN.
          // val.replace(qemuHost, "10.0.2.2");
          // b/c the system property should be a FQDN (just like the test args are FQDN)
          val = val.replace(serverHost, "10.0.2.2");
        }
      }
      qemuArgMap.put(key, val);
    }
    if ("true"
        .equalsIgnoreCase(cleanArgMap.get(TestStorageConstants.USE_QEMU_IPS_IF_POSSIBLE_ARG_TAG))) {
      return qemuArgMap;
    } else {
      return cleanArgMap;
    }
  }

  private static TestArguments readProtoFromFile() {
    File testArgsFile =
        new File(
            Environment.getExternalStorageDirectory(),
            TestStorageConstants.ON_DEVICE_PATH_INTERNAL_USE
                + TestStorageConstants.TEST_ARGS_FILE_NAME);
    if (!testArgsFile.exists()) {
      return TestArguments.getDefaultInstance();
    }
    try {
      return TestArguments.parseFrom(new FileInputStream(testArgsFile));
    } catch (IOException e) {
      throw new RuntimeException("Not able to read from file: " + testArgsFile.getName(), e);
    }
  }

  @Override
  public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
    // Not allowed.
    return 0;
  }
}