content: /*
   * Copyright (C) 2012 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.webkit;
  
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.content.pm.PackageInfo;
  import android.content.pm.PackageManager;
  import android.os.Binder;
  import android.os.PatternMatcher;
  import android.os.Process;
  import android.os.ResultReceiver;
  import android.os.ShellCallback;
  import android.os.UserHandle;
  import android.util.Slog;
  import android.webkit.IWebViewUpdateService;
  import android.webkit.WebViewProviderInfo;
  import android.webkit.WebViewProviderResponse;
  
  import com.android.internal.util.DumpUtils;
  import com.android.server.SystemService;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.util.Arrays;
  
  /**
   * Private service to wait for the updatable WebView to be ready for use.
   * @hide
   */
  public class WebViewUpdateService extends SystemService {
  
      private static final String TAG = "WebViewUpdateService";
  
      private BroadcastReceiver mWebViewUpdatedReceiver;
      private WebViewUpdateServiceImpl mImpl;
  
      static final int PACKAGE_CHANGED = 0;
      static final int PACKAGE_ADDED = 1;
      static final int PACKAGE_ADDED_REPLACED = 2;
      static final int PACKAGE_REMOVED = 3;
  
      public WebViewUpdateService(Context context) {
          super(context);
          mImpl = new WebViewUpdateServiceImpl(context, SystemImpl.getInstance());
      }
  
      @Override
      public void onStart() {
          mWebViewUpdatedReceiver = new BroadcastReceiver() {
                  @Override
                  public void onReceive(Context context, Intent intent) {
                      int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
                      switch (intent.getAction()) {
                          case Intent.ACTION_PACKAGE_REMOVED:
                              // When a package is replaced we will receive two intents, one
                              // representing the removal of the old package and one representing the
                              // addition of the new package.
                              // In the case where we receive an intent to remove the old version of
                              // the package that is being replaced we early-out here so that we don't
                              // run the update-logic twice.
                              if (intent.getExtras().getBoolean(Intent.EXTRA_REPLACING)) return;
                              mImpl.packageStateChanged(packageNameFromIntent(intent),
                                      PACKAGE_REMOVED, userId);
                              break;
                          case Intent.ACTION_PACKAGE_CHANGED:
                              // Ensure that we only heed PACKAGE_CHANGED intents if they change an
                              // entire package, not just a component
                              if (entirePackageChanged(intent)) {
                                  mImpl.packageStateChanged(packageNameFromIntent(intent),
                                          PACKAGE_CHANGED, userId);
                              }
                              break;
                          case Intent.ACTION_PACKAGE_ADDED:
                              mImpl.packageStateChanged(packageNameFromIntent(intent),
                                      (intent.getExtras().getBoolean(Intent.EXTRA_REPLACING)
                                       ? PACKAGE_ADDED_REPLACED : PACKAGE_ADDED), userId);
                              break;
                          case Intent.ACTION_USER_STARTED:
                              mImpl.handleNewUser(userId);
                              break;
                          case Intent.ACTION_USER_REMOVED:
                              mImpl.handleUserRemoved(userId);
                              break;
                      }
                  }
          };
          IntentFilter filter = new IntentFilter();
          filter.addAction(Intent.ACTION_PACKAGE_ADDED);
          filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
          filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
          filter.addDataScheme("package");
          // Make sure we only receive intents for WebView packages from our config file.
          for (WebViewProviderInfo provider : mImpl.getWebViewPackages()) {
              filter.addDataSchemeSpecificPart(provider.packageName, PatternMatcher.PATTERN_LITERAL);
          }
  
          getContext().registerReceiverAsUser(mWebViewUpdatedReceiver, UserHandle.ALL, filter,
                  null /* broadcast permission */, null /* handler */);
  
          IntentFilter userAddedFilter = new IntentFilter();
          userAddedFilter.addAction(Intent.ACTION_USER_STARTED);
          userAddedFilter.addAction(Intent.ACTION_USER_REMOVED);
          getContext().registerReceiverAsUser(mWebViewUpdatedReceiver, UserHandle.ALL,
                  userAddedFilter, null /* broadcast permission */, null /* handler */);
  
          publishBinderService("webviewupdate", new BinderService(), true /*allowIsolated*/);
      }
  
      public void prepareWebViewInSystemServer() {
          mImpl.prepareWebViewInSystemServer();
      }
  
      private static String packageNameFromIntent(Intent intent) {
          return intent.getDataString().substring("package:".length());
      }
  
      /**
       * Returns whether the entire package from an ACTION_PACKAGE_CHANGED intent was changed (rather
       * than just one of its components).
       * @hide
       */
      public static boolean entirePackageChanged(Intent intent) {
          String[] componentList =
              intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
          return Arrays.asList(componentList).contains(
                  intent.getDataString().substring("package:".length()));
      }
  
      private class BinderService extends IWebViewUpdateService.Stub {
  
          @Override
          public void onShellCommand(FileDescriptor in, FileDescriptor out,
                  FileDescriptor err, String[] args, ShellCallback callback,
                  ResultReceiver resultReceiver) {
              (new WebViewUpdateServiceShellCommand(this)).exec(
                      this, in, out, err, args, callback, resultReceiver);
          }
  
  
          /**
           * The shared relro process calls this to notify us that it's done trying to create a relro
           * file. This method gets called even if the relro creation has failed or the process
           * crashed.
           */
          @Override // Binder call
          public void notifyRelroCreationCompleted() {
              // Verify that the caller is either the shared relro process (nominal case) or the
              // system server (only in the case the relro process crashes and we get here via the
              // crashHandler).
              if (Binder.getCallingUid() != Process.SHARED_RELRO_UID &&
                      Binder.getCallingUid() != Process.SYSTEM_UID) {
                  return;
              }
  
              long callingId = Binder.clearCallingIdentity();
              try {
                  WebViewUpdateService.this.mImpl.notifyRelroCreationCompleted();
              } finally {
                  Binder.restoreCallingIdentity(callingId);
              }
          }
  
          /**
           * WebViewFactory calls this to block WebView loading until the relro file is created.
           * Returns the WebView provider for which we create relro files.
           */
          @Override // Binder call
          public WebViewProviderResponse waitForAndGetProvider() {
              // The WebViewUpdateService depends on the prepareWebViewInSystemServer call, which
              // happens later (during the PHASE_ACTIVITY_MANAGER_READY) in SystemServer.java. If
              // another service there tries to bring up a WebView in the between, the wait below
              // would deadlock without the check below.
              if (Binder.getCallingPid() == Process.myPid()) {
                  throw new IllegalStateException("Cannot create a WebView from the SystemServer");
              }
  
              return WebViewUpdateService.this.mImpl.waitForAndGetProvider();
          }
  
          /**
           * This is called from DeveloperSettings when the user changes WebView provider.
           */
          @Override // Binder call
          public String changeProviderAndSetting(String newProvider) {
              if (getContext().checkCallingPermission(
                          android.Manifest.permission.WRITE_SECURE_SETTINGS)
                      != PackageManager.PERMISSION_GRANTED) {
                  String msg = "Permission Denial: changeProviderAndSetting() from pid="
                          + Binder.getCallingPid()
                          + ", uid=" + Binder.getCallingUid()
                          + " requires " + android.Manifest.permission.WRITE_SECURE_SETTINGS;
                  Slog.w(TAG, msg);
                  throw new SecurityException(msg);
              }
  
              long callingId = Binder.clearCallingIdentity();
              try {
                  return WebViewUpdateService.this.mImpl.changeProviderAndSetting(
                          newProvider);
              } finally {
                  Binder.restoreCallingIdentity(callingId);
              }
          }
  
          @Override // Binder call
          public WebViewProviderInfo[] getValidWebViewPackages() {
              return WebViewUpdateService.this.mImpl.getValidWebViewPackages();
          }
  
          @Override // Binder call
          public WebViewProviderInfo[] getAllWebViewPackages() {
              return WebViewUpdateService.this.mImpl.getWebViewPackages();
          }
  
          @Override // Binder call
          public String getCurrentWebViewPackageName() {
              PackageInfo pi = WebViewUpdateService.this.mImpl.getCurrentWebViewPackage();
              return pi == null ? null : pi.packageName;
          }
  
          @Override // Binder call
          public PackageInfo getCurrentWebViewPackage() {
              return WebViewUpdateService.this.mImpl.getCurrentWebViewPackage();
          }
  
          @Override // Binder call
          public boolean isMultiProcessEnabled() {
              return WebViewUpdateService.this.mImpl.isMultiProcessEnabled();
          }
  
          @Override // Binder call
          public void enableMultiProcess(boolean enable) {
              if (getContext().checkCallingPermission(
                          android.Manifest.permission.WRITE_SECURE_SETTINGS)
                      != PackageManager.PERMISSION_GRANTED) {
                  String msg = "Permission Denial: enableMultiProcess() from pid="
                          + Binder.getCallingPid()
                          + ", uid=" + Binder.getCallingUid()
                          + " requires " + android.Manifest.permission.WRITE_SECURE_SETTINGS;
                  Slog.w(TAG, msg);
                  throw new SecurityException(msg);
              }
  
              long callingId = Binder.clearCallingIdentity();
              try {
                  WebViewUpdateService.this.mImpl.enableMultiProcess(enable);
              } finally {
                  Binder.restoreCallingIdentity(callingId);
              }
          }
  
          @Override
          protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
              if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return;
              WebViewUpdateService.this.mImpl.dumpState(pw);
          }
      }
  }
  