1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.android.setupcompat.internal; 18 19 import android.annotation.SuppressLint; 20 import android.content.Context; 21 import android.os.Bundle; 22 import android.os.RemoteException; 23 import androidx.annotation.VisibleForTesting; 24 import com.google.android.setupcompat.ISetupCompatService; 25 import com.google.android.setupcompat.logging.internal.SetupMetricsLoggingConstants.MetricType; 26 import com.google.android.setupcompat.util.Logger; 27 import java.util.concurrent.ExecutorService; 28 import java.util.concurrent.RejectedExecutionException; 29 import java.util.concurrent.TimeUnit; 30 import java.util.concurrent.TimeoutException; 31 32 /** 33 * This class is responsible for safely executing methods on SetupCompatService. To avoid memory 34 * issues due to backed up queues, an upper bound of {@link 35 * ExecutorProvider#SETUP_METRICS_LOGGER_MAX_QUEUED} is set on the logging executor service's queue. 36 * Once the upper bound is reached, metrics published after this event are dropped silently. 37 * 38 * <p>NOTE: This class is not meant to be used directly. Please use {@link 39 * com.google.android.setupcompat.logging.SetupMetricsLogger} for publishing metric events. 40 */ 41 public class SetupCompatServiceInvoker { 42 43 private static final Logger LOG = new Logger("SetupCompatServiceInvoker"); 44 45 @SuppressLint("DefaultLocale") logMetricEvent(@etricType int metricType, Bundle args)46 public void logMetricEvent(@MetricType int metricType, Bundle args) { 47 try { 48 loggingExecutor.execute(() -> invokeLogMetric(metricType, args)); 49 } catch (RejectedExecutionException e) { 50 LOG.e(String.format("Metric of type %d dropped since queue is full.", metricType), e); 51 } 52 } 53 54 /** 55 * Help invoke the {@link ISetupCompatService#onFocusStatusChanged} using {@code loggingExecutor}. 56 */ onFocusStatusChanged(String screenName, Bundle bundle)57 public void onFocusStatusChanged(String screenName, Bundle bundle) { 58 try { 59 loggingExecutor.execute(() -> invokeOnWindowFocusChanged(screenName, bundle)); 60 } catch (RejectedExecutionException e) { 61 LOG.e(String.format("Screen %s report focus changed failed.", screenName), e); 62 } 63 } 64 invokeLogMetric( @etricType int metricType, @SuppressWarnings("unused") Bundle args)65 private void invokeLogMetric( 66 @MetricType int metricType, @SuppressWarnings("unused") Bundle args) { 67 try { 68 ISetupCompatService setupCompatService = 69 SetupCompatServiceProvider.get( 70 context, waitTimeInMillisForServiceConnection, TimeUnit.MILLISECONDS); 71 if (setupCompatService != null) { 72 setupCompatService.logMetric(metricType, args, Bundle.EMPTY); 73 } else { 74 LOG.w("logMetric failed since service reference is null. Are the permissions valid?"); 75 } 76 } catch (InterruptedException | TimeoutException | RemoteException | IllegalStateException e) { 77 LOG.e(String.format("Exception occurred while trying to log metric = [%s]", args), e); 78 } 79 } 80 invokeOnWindowFocusChanged(String screenName, Bundle bundle)81 private void invokeOnWindowFocusChanged(String screenName, Bundle bundle) { 82 try { 83 ISetupCompatService setupCompatService = 84 SetupCompatServiceProvider.get( 85 context, waitTimeInMillisForServiceConnection, TimeUnit.MILLISECONDS); 86 if (setupCompatService != null) { 87 setupCompatService.onFocusStatusChanged(bundle); 88 } else { 89 LOG.w( 90 "Report focusChange failed since service reference is null. Are the permission valid?"); 91 } 92 } catch (InterruptedException 93 | TimeoutException 94 | RemoteException 95 | UnsupportedOperationException e) { 96 LOG.e( 97 String.format( 98 "Exception occurred while %s trying report windowFocusChange to SetupWizard.", 99 screenName), 100 e); 101 } 102 } 103 SetupCompatServiceInvoker(Context context)104 private SetupCompatServiceInvoker(Context context) { 105 this.context = context; 106 this.loggingExecutor = ExecutorProvider.setupCompatServiceInvoker.get(); 107 this.waitTimeInMillisForServiceConnection = MAX_WAIT_TIME_FOR_CONNECTION_MS; 108 } 109 110 private final Context context; 111 112 private final ExecutorService loggingExecutor; 113 private final long waitTimeInMillisForServiceConnection; 114 get(Context context)115 public static synchronized SetupCompatServiceInvoker get(Context context) { 116 if (instance == null) { 117 instance = new SetupCompatServiceInvoker(context.getApplicationContext()); 118 } 119 120 return instance; 121 } 122 123 @VisibleForTesting setInstanceForTesting(SetupCompatServiceInvoker testInstance)124 static void setInstanceForTesting(SetupCompatServiceInvoker testInstance) { 125 instance = testInstance; 126 } 127 128 // The instance is coming from Application context which alive during the application activate and 129 // it's not depend on the activities life cycle, so we can avoid memory leak. However linter 130 // cannot distinguish Application context or activity context, so we add @SuppressLint to avoid 131 // lint error. 132 @SuppressLint("StaticFieldLeak") 133 private static SetupCompatServiceInvoker instance; 134 135 private static final long MAX_WAIT_TIME_FOR_CONNECTION_MS = TimeUnit.SECONDS.toMillis(10); 136 } 137