content: /*
   * Copyright (C) 2013 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.location;
  
  import android.annotation.NonNull;
  import android.app.Service;
  import android.content.Context;
  import android.content.Intent;
  import android.os.Bundle;
  import android.os.IBinder;
  import android.os.Message;
  import android.os.Messenger;
  import android.os.RemoteException;
  import android.util.Log;
  
  /**
   * Dynamically specifies the summary (subtitle) and enabled status of a preference injected into
   * the list of app settings displayed by the system settings app
   * <p/>
   * For use only by apps that are included in the system image, for preferences that affect multiple
   * apps. Location settings that apply only to one app should be shown within that app,
   * rather than in the system settings.
   * <p/>
   * To add a preference to the list, a subclass of {@link SettingInjectorService} must be declared in
   * the manifest as so:
   *
   * <pre>
   *     &lt;service android:name="com.example.android.injector.MyInjectorService" &gt;
   *         &lt;intent-filter&gt;
   *             &lt;action android:name="android.location.SettingInjectorService" /&gt;
   *         &lt;/intent-filter&gt;
   *
   *         &lt;meta-data
   *             android:name="android.location.SettingInjectorService"
   *             android:resource="@xml/my_injected_location_setting" /&gt;
   *     &lt;/service&gt;
   * </pre>
   * The resource file specifies the static data for the setting:
   * <pre>
   *     &lt;injected-location-setting xmlns:android="http://schemas.android.com/apk/res/android"
   *         android:title="@string/injected_setting_title"
   *         android:icon="@drawable/ic_acme_corp"
   *         android:settingsActivity="com.example.android.injector.MySettingActivity"
   *     /&gt;
   * </pre>
   * Here:
   * <ul>
   * <li>title: The {@link android.preference.Preference#getTitle()} value. The title should make
   * it clear which apps are affected by the setting, typically by including the name of the
   * developer. For example, "Acme Corp. ads preferences." </li>
   *
   * <li>icon: The {@link android.preference.Preference#getIcon()} value. Typically this will be a
   * generic icon for the developer rather than the icon for an individual app.</li>
   *
   * <li>settingsActivity: the activity which is launched to allow the user to modify the setting
   * value.  The activity must be in the same package as the subclass of
   * {@link SettingInjectorService}. The activity should use your own branding to help emphasize
   * to the user that it is not part of the system settings.</li>
   * </ul>
   *
   * To ensure a good user experience, your {@link android.app.Application#onCreate()},
   * {@link #onGetSummary()}, and {@link #onGetEnabled()} methods must all be fast. If any are slow,
   * it can delay the display of settings values for other apps as well. Note further that all are
   * called on your app's UI thread.
   * <p/>
   * For compactness, only one copy of a given setting should be injected. If each account has a
   * distinct value for the setting, then the {@link #onGetSummary()} value should represent a summary
   * of the state across all of the accounts and {@code settingsActivity} should display the value for
   * each account.
   */
  public abstract class SettingInjectorService extends Service {
  
      private static final String TAG = "SettingInjectorService";
  
      /**
       * Intent action that must be declared in the manifest for the subclass. Used to start the
       * service to read the dynamic status for the setting.
       */
      public static final String ACTION_SERVICE_INTENT = "android.location.SettingInjectorService";
  
      /**
       * Name of the meta-data tag used to specify the resource file that includes the settings
       * attributes.
       */
      public static final String META_DATA_NAME = "android.location.SettingInjectorService";
  
      /**
       * Name of the XML tag that includes the attributes for the setting.
       */
      public static final String ATTRIBUTES_NAME = "injected-location-setting";
  
      /**
       * Intent action a client should broadcast when the value of one of its injected settings has
       * changed, so that the setting can be updated in the UI.
       */
      public static final String ACTION_INJECTED_SETTING_CHANGED =
              "android.location.InjectedSettingChanged";
  
      /**
       * Name of the bundle key for the string specifying the summary for the setting (e.g., "ON" or
       * "OFF").
       *
       * @hide
       */
      public static final String SUMMARY_KEY = "summary";
  
      /**
       * Name of the bundle key for the string specifying whether the setting is currently enabled.
       *
       * @hide
       */
      public static final String ENABLED_KEY = "enabled";
  
      /**
       * Name of the intent key used to specify the messenger
       *
       * @hide
       */
      public static final String MESSENGER_KEY = "messenger";
  
      private final String mName;
  
      /**
       * Constructor.
       *
       * @param name used to identify your subclass in log messages
       */
      public SettingInjectorService(String name) {
          mName = name;
      }
  
      @Override
      public final IBinder onBind(Intent intent) {
          return null;
      }
  
      @Override
      public final void onStart(Intent intent, int startId) {
          super.onStart(intent, startId);
      }
  
      @Override
      public final int onStartCommand(Intent intent, int flags, int startId) {
          onHandleIntent(intent);
          stopSelf(startId);
          return START_NOT_STICKY;
      }
  
      private void onHandleIntent(Intent intent) {
          String summary = null;
          boolean enabled = false;
          try {
              summary = onGetSummary();
              enabled = onGetEnabled();
          } finally {
              // If exception happens, send status anyway, so that settings injector can immediately
              // start loading the status of the next setting. But leave the exception uncaught to
              // crash the injector service itself.
              sendStatus(intent, summary, enabled);
          }
      }
  
      /**
       * Send the enabled values back to the caller via the messenger encoded in the
       * intent.
       */
      private void sendStatus(Intent intent, String summary, boolean enabled) {
          Messenger messenger = intent.getParcelableExtra(MESSENGER_KEY);
          // Bail out to avoid crashing GmsCore with incoming malicious Intent.
          if (messenger == null) {
              return;
          }
  
          Message message = Message.obtain();
          Bundle bundle = new Bundle();
          bundle.putString(SUMMARY_KEY, summary);
          bundle.putBoolean(ENABLED_KEY, enabled);
          message.setData(bundle);
  
          if (Log.isLoggable(TAG, Log.DEBUG)) {
              Log.d(TAG, mName + ": received " + intent + ", summary=" + summary
                      + ", enabled=" + enabled + ", sending message: " + message);
          }
  
          try {
              messenger.send(message);
          } catch (RemoteException e) {
              Log.e(TAG, mName + ": sending dynamic status failed", e);
          }
      }
  
      /**
       * Returns the {@link android.preference.Preference#getSummary()} value (allowed to be null or
       * empty). Should not perform unpredictably-long operations such as network access--see the
       * running-time comments in the class-level javadoc.
       * <p/>
       * This method is called on KitKat, and Q+ devices.
       *
       * @return the {@link android.preference.Preference#getSummary()} value
       */
      protected abstract String onGetSummary();
  
      /**
       * Returns the {@link android.preference.Preference#isEnabled()} value. Should not perform
       * unpredictably-long operations such as network access--see the running-time comments in the
       * class-level javadoc.
       * <p/>
       * Note that to prevent churn in the settings list, there is no support for dynamically choosing
       * to hide a setting. Instead you should have this method return false, which will disable the
       * setting and its link to your setting activity. One reason why you might choose to do this is
       * if {@link android.provider.Settings.Secure#LOCATION_MODE} is {@link
       * android.provider.Settings.Secure#LOCATION_MODE_OFF}.
       * <p/>
       * It is possible that the user may click on the setting before this method returns, so your
       * settings activity must handle the case where it is invoked even though the setting is
       * disabled. The simplest approach may be to simply call {@link android.app.Activity#finish()}
       * when disabled.
       *
       * @return the {@link android.preference.Preference#isEnabled()} value
       */
      protected abstract boolean onGetEnabled();
  
      /**
       * Sends a broadcast to refresh the injected settings on location settings page.
       */
      public static final void refreshSettings(@NonNull Context context) {
          Intent intent = new Intent(ACTION_INJECTED_SETTING_CHANGED);
          context.sendBroadcast(intent);
      }
  }
  